From 2b058a2533655d7ce6275ab1dd3829661a94198f Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 17 Jan 2025 13:50:43 -0500 Subject: [PATCH 01/29] PYTHON-5044 - Successive AsyncMongoClients on a single loop always timeout on server selection --- pymongo/asynchronous/mongo_client.py | 2 ++ pymongo/periodic_executor.py | 2 ++ pymongo/synchronous/mongo_client.py | 1 + 3 files changed, 5 insertions(+) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 1600e50628..1f75353112 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1565,6 +1565,8 @@ async def close(self) -> None: # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. await self._encrypter.close() self._closed = True + # Yield to the asyncio event loop so all executor tasks properly exit after cancellation + await asyncio.sleep(0) if not _IS_SYNC: # Add support for contextlib.aclosing. diff --git a/pymongo/periodic_executor.py b/pymongo/periodic_executor.py index 9b10f6e7e3..b3756adeef 100644 --- a/pymongo/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -75,6 +75,8 @@ def close(self, dummy: Any = None) -> None: callback; see monitor.py. """ self._stopped = True + if self._task: + self._task.cancel() async def join(self, timeout: Optional[int] = None) -> None: if self._task is not None: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index a694a58c1e..92cfe78713 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -1559,6 +1559,7 @@ def close(self) -> None: # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. self._encrypter.close() self._closed = True + # Yield to the asyncio event loop so all executor tasks properly exit after cancellation if not _IS_SYNC: # Add support for contextlib.closing. From 45e74dab3fb4f9924bc22ab1933ef96bfe516811 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 21 Jan 2025 11:08:53 -0500 Subject: [PATCH 02/29] Only join executors on async --- pymongo/asynchronous/mongo_client.py | 4 ++-- pymongo/asynchronous/monitor.py | 4 ++++ pymongo/periodic_executor.py | 2 +- pymongo/synchronous/mongo_client.py | 3 ++- pymongo/synchronous/monitor.py | 4 ++++ test/__init__.py | 2 -- test/asynchronous/__init__.py | 2 -- 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 1f75353112..0026827108 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1559,14 +1559,14 @@ async def close(self) -> None: # Stop the periodic task thread and then send pending killCursor # requests before closing the topology. self._kill_cursors_executor.close() + if not _IS_SYNC: + await self._kill_cursors_executor.join() await self._process_kill_cursors() await self._topology.close() if self._encrypter: # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. await self._encrypter.close() self._closed = True - # Yield to the asyncio event loop so all executor tasks properly exit after cancellation - await asyncio.sleep(0) if not _IS_SYNC: # Add support for contextlib.aclosing. diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index ad1bc70aba..de22a30780 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -191,6 +191,8 @@ def gc_safe_close(self) -> None: async def close(self) -> None: self.gc_safe_close() + if not _IS_SYNC: + await self._executor.join() await self._rtt_monitor.close() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. @@ -458,6 +460,8 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool async def close(self) -> None: self.gc_safe_close() + if not _IS_SYNC: + await self._executor.join() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. await self._pool.reset() diff --git a/pymongo/periodic_executor.py b/pymongo/periodic_executor.py index b3756adeef..f51a988728 100644 --- a/pymongo/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -75,7 +75,7 @@ def close(self, dummy: Any = None) -> None: callback; see monitor.py. """ self._stopped = True - if self._task: + if self._task is not None: self._task.cancel() async def join(self, timeout: Optional[int] = None) -> None: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 92cfe78713..1b8f9dc5f7 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -1553,13 +1553,14 @@ def close(self) -> None: # Stop the periodic task thread and then send pending killCursor # requests before closing the topology. self._kill_cursors_executor.close() + if not _IS_SYNC: + self._kill_cursors_executor.join() self._process_kill_cursors() self._topology.close() if self._encrypter: # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. self._encrypter.close() self._closed = True - # Yield to the asyncio event loop so all executor tasks properly exit after cancellation if not _IS_SYNC: # Add support for contextlib.closing. diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index df4130d4ab..5558c5fd07 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -191,6 +191,8 @@ def gc_safe_close(self) -> None: def close(self) -> None: self.gc_safe_close() + if not _IS_SYNC: + self._executor.join() self._rtt_monitor.close() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. @@ -458,6 +460,8 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool def close(self) -> None: self.gc_safe_close() + if not _IS_SYNC: + self._executor.join() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. self._pool.reset() diff --git a/test/__init__.py b/test/__init__.py index d3a63db2d5..f165a7cc72 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1136,8 +1136,6 @@ class IntegrationTest(PyMongoTestCase): @client_context.require_connection def setUp(self) -> None: - if not _IS_SYNC: - reset_client_context() if client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") if client_context.serverless and not getattr(self, "RUN_ON_SERVERLESS", False): diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 73e2824742..3d82d90792 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -1154,8 +1154,6 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase): @async_client_context.require_connection async def asyncSetUp(self) -> None: - if not _IS_SYNC: - await reset_client_context() if async_client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") if async_client_context.serverless and not getattr(self, "RUN_ON_SERVERLESS", False): From 0296c2022121999eb0ac53c61212aa6b6756d2de Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 21 Jan 2025 11:19:19 -0500 Subject: [PATCH 03/29] Remove unneeded reset_async_client_context --- test/__init__.py | 10 ---------- test/asynchronous/__init__.py | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index f165a7cc72..ed7966f718 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -864,16 +864,6 @@ def max_message_size_bytes(self): client_context = ClientContext() -def reset_client_context(): - if _IS_SYNC: - # sync tests don't need to reset a client context - return - elif client_context.client is not None: - client_context.client.close() - client_context.client = None - client_context._init_client() - - class PyMongoTestCase(unittest.TestCase): def assertEqualCommand(self, expected, actual, msg=None): self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 3d82d90792..45db6fcd9a 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -866,16 +866,6 @@ async def max_message_size_bytes(self): async_client_context = AsyncClientContext() -async def reset_client_context(): - if _IS_SYNC: - # sync tests don't need to reset a client context - return - elif async_client_context.client is not None: - await async_client_context.client.close() - async_client_context.client = None - await async_client_context._init_client() - - class AsyncPyMongoTestCase(unittest.IsolatedAsyncioTestCase): def assertEqualCommand(self, expected, actual, msg=None): self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg) From 266b0a3b150c2712db1032ed8277320e2dedc4a9 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 21 Jan 2025 12:19:10 -0500 Subject: [PATCH 04/29] Convert test_client to pytest --- pymongo/asynchronous/mongo_client.py | 1 + pyproject.toml | 2 +- test/asynchronous/__init__.py | 75 ++- test/asynchronous/conftest.py | 103 +++- test/asynchronous/test_client_pytest.py | 674 ++++++++++++++++++++++++ test/pytest_conf.py | 4 +- 6 files changed, 839 insertions(+), 20 deletions(-) create mode 100644 test/asynchronous/test_client_pytest.py diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 0026827108..6a83de4cc1 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1418,6 +1418,7 @@ def __next__(self) -> NoReturn: raise TypeError("'AsyncMongoClient' object is not iterable") next = __next__ + anext = next async def _server_property(self, attr_name: str) -> Any: """An attribute of the current server's description. diff --git a/pyproject.toml b/pyproject.toml index 69249ee4c6..d25bed8dd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ zstd = ["requirements/zstd.txt"] [tool.pytest.ini_options] minversion = "7" -addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async"] +addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async or asyncio"] testpaths = ["test"] log_cli_level = "INFO" faulthandler_timeout = 1500 diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 45db6fcd9a..e335755990 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -30,6 +30,10 @@ import unittest import warnings from asyncio import iscoroutinefunction + +import pytest_asyncio +from pymongo.lock import _create_lock, _async_create_lock + from test.helpers import ( COMPRESSORS, IS_SRV, @@ -116,7 +120,7 @@ def __init__(self): self.default_client_options: Dict = {} self.sessions_enabled = False self.client = None # type: ignore - self.conn_lock = threading.Lock() + self.conn_lock = _async_create_lock() self.is_data_lake = False self.load_balancer = TEST_LOADBALANCER self.serverless = TEST_SERVERLESS @@ -337,7 +341,7 @@ async def _init_client(self): await mongos_client.close() async def init(self): - with self.conn_lock: + async with self.conn_lock: if not self.client and not self.connection_attempts: await self._init_client() @@ -520,6 +524,12 @@ def require_data_lake(self, func): func=func, ) + @property + def is_not_mmap(self): + if self.is_mongos: + return True + return self.storage_engine != "mmapv1" + def require_no_mmap(self, func): """Run a test only if the server is not using the MMAPv1 storage engine. Only works for standalone and replica sets; tests are @@ -573,6 +583,10 @@ def require_replica_set(self, func): """Run a test only if the client is connected to a replica set.""" return self._require(lambda: self.is_rs, "Not connected to a replica set", func=func) + @property + async def secondaries_count(self): + return 0 if not self.client else len(await self.client.secondaries) + def require_secondaries_count(self, count): """Run a test only if the client is connected to a replica set that has `count` secondaries. @@ -588,7 +602,7 @@ async def check(): @property async def supports_secondary_read_pref(self): - if self.has_secondaries: + if await self.has_secondaries: return True if self.is_mongos: shard = await self.client.config.shards.find_one()["host"] # type:ignore[index] @@ -692,7 +706,7 @@ async def is_topology_type(self, topologies): if "sharded" in topologies and self.is_mongos: return True if "sharded-replicaset" in topologies and self.is_mongos: - shards = await async_client_context.client.config.shards.find().to_list() + shards = await self.client.config.shards.find().to_list() for shard in shards: # For a 3-member RS-backed sharded cluster, shard['host'] # will be 'replicaName/ip1:port1,ip2:port2,ip3:port3' @@ -1191,8 +1205,39 @@ async def asyncTearDown(self) -> None: await super().asyncTearDown() +async def _get_environment(): + client = AsyncClientContext() + await client.init() + requirements = {} + requirements["SUPPORT_TRANSACTIONS"] = client.supports_transactions() + requirements["IS_DATA_LAKE"] = client.is_data_lake + requirements["IS_SYNC"] = _IS_SYNC + requirements["IS_SYNC"] = _IS_SYNC + requirements["REQUIRE_API_VERSION"] = MONGODB_API_VERSION + requirements["SUPPORTS_FAILCOMMAND_FAIL_POINT"] = client.supports_failCommand_fail_point + requirements["IS_NOT_MMAP"] = client.is_not_mmap + requirements["SERVER_VERSION"] = client.version + requirements["AUTH_ENABLED"] = client.auth_enabled + requirements["FIPS_ENABLED"] = client.fips_enabled + requirements["IS_RS"] = client.is_rs + requirements["MONGOSES"] = len(client.mongoses) + requirements["SECONDARIES_COUNT"] = await client.secondaries_count + requirements["SECONDARY_READ_PREF"] = await client.supports_secondary_read_pref + requirements["HAS_IPV6"] = client.has_ipv6 + requirements["IS_SERVERLESS"] = client.serverless + requirements["IS_LOAD_BALANCER"] = client.load_balancer + requirements["TEST_COMMANDS_ENABLED"] = client.test_commands_enabled + requirements["IS_TLS"] = client.tls + requirements["IS_TLS_CERT"] = client.tlsCertificateKeyFile + requirements["SERVER_IS_RESOLVEABLE"] = client.server_is_resolvable + requirements["SESSIONS_ENABLED"] = client.sessions_enabled + requirements["SUPPORTS_RETRYABLE_WRITES"] = client.supports_retryable_writes() + await client.client.close() + + return requirements + async def async_setup(): - await async_client_context.init() + await _get_environment() warnings.resetwarnings() warnings.simplefilter("always") global_knobs.enable() @@ -1207,16 +1252,16 @@ async def async_teardown(): garbage.append(f" gc.get_referrers: {gc.get_referrers(g)!r}") if garbage: raise AssertionError("\n".join(garbage)) - c = async_client_context.client - if c: - if not async_client_context.is_data_lake: - await c.drop_database("pymongo-pooling-tests") - await c.drop_database("pymongo_test") - await c.drop_database("pymongo_test1") - await c.drop_database("pymongo_test2") - await c.drop_database("pymongo_test_mike") - await c.drop_database("pymongo_test_bernie") - await c.close() + # c = async_client_context.client + # if c: + # if not async_client_context.is_data_lake: + # await c.drop_database("pymongo-pooling-tests") + # await c.drop_database("pymongo_test") + # await c.drop_database("pymongo_test1") + # await c.drop_database("pymongo_test2") + # await c.drop_database("pymongo_test_mike") + # await c.drop_database("pymongo_test_bernie") + # await c.close() print_running_clients() diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index a27a9f213d..ab3c3e7bc0 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -2,8 +2,14 @@ import asyncio import sys -from test import pytest_conf -from test.asynchronous import async_setup, async_teardown + +from typing_extensions import Any + +from pymongo import AsyncMongoClient +from pymongo.uri_parser import parse_uri + +from test import pytest_conf, db_user, db_pwd +from test.asynchronous import async_setup, async_teardown, _connection_string, AsyncClientContext import pytest import pytest_asyncio @@ -21,12 +27,103 @@ def event_loop_policy(): return asyncio.get_event_loop_policy() +@pytest_asyncio.fixture(loop_scope="session") +async def async_client_context_fixture(): + client = AsyncClientContext() + await client.init() + yield client + await client.client.close() -@pytest_asyncio.fixture(scope="package", autouse=True) +@pytest_asyncio.fixture(loop_scope="session", autouse=True) async def test_setup_and_teardown(): await async_setup() yield await async_teardown() +async def _async_mongo_client( + async_client_context, host, port, authenticate=True, directConnection=None, **kwargs + ): + """Create a new client over SSL/TLS if necessary.""" + host = host or await async_client_context.host + port = port or await async_client_context.port + client_options: dict = async_client_context.default_client_options.copy() + if async_client_context.replica_set_name and not directConnection: + client_options["replicaSet"] = async_client_context.replica_set_name + if directConnection is not None: + client_options["directConnection"] = directConnection + client_options.update(kwargs) + + uri = _connection_string(host) + auth_mech = kwargs.get("authMechanism", "") + if async_client_context.auth_enabled and authenticate and auth_mech != "MONGODB-OIDC": + # Only add the default username or password if one is not provided. + res = parse_uri(uri) + if ( + not res["username"] + and not res["password"] + and "username" not in client_options + and "password" not in client_options + ): + client_options["username"] = db_user + client_options["password"] = db_pwd + client = AsyncMongoClient(uri, port, **client_options) + if client._options.connect: + await client.aconnect() + return client + + +async def async_single_client_noauth( + async_client_context, h: Any = None, p: Any = None, **kwargs: Any +) -> AsyncMongoClient[dict]: + """Make a direct connection. Don't authenticate.""" + return await _async_mongo_client(async_client_context, h, p, authenticate=False, directConnection=True, **kwargs) +# +async def async_single_client( + async_client_context, h: Any = None, p: Any = None, **kwargs: Any +) -> AsyncMongoClient[dict]: + """Make a direct connection, and authenticate if necessary.""" + return await _async_mongo_client(async_client_context, h, p, directConnection=True, **kwargs) + +# @pytest_asyncio.fixture(loop_scope="function") +# async def async_rs_client_noauth( +# async_client_context, h: Any = None, p: Any = None, **kwargs: Any +# ) -> AsyncMongoClient[dict]: +# """Connect to the replica set. Don't authenticate.""" +# return await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) +# +# @pytest_asyncio.fixture(loop_scope="function") +# async def async_rs_client( +# async_client_context, h: Any = None, p: Any = None, **kwargs: Any +# ) -> AsyncMongoClient[dict]: +# """Connect to the replica set and authenticate if necessary.""" +# return await _async_mongo_client(async_client_context, h, p, **kwargs) +# +# @pytest_asyncio.fixture(loop_scope="function") +# async def async_rs_or_single_client_noauth( +# async_client_context, h: Any = None, p: Any = None, **kwargs: Any +# ) -> AsyncMongoClient[dict]: +# """Connect to the replica set if there is one, otherwise the standalone. +# +# Like rs_or_single_client, but does not authenticate. +# """ +# return await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) + +async def async_rs_or_single_client( + async_client_context, h: Any = None, p: Any = None, **kwargs: Any +) -> AsyncMongoClient[Any]: + """Connect to the replica set if there is one, otherwise the standalone. + + Authenticates if necessary. + """ + return await _async_mongo_client(async_client_context, h, p, **kwargs) + +def simple_client(h: Any = None, p: Any = None, **kwargs: Any) -> AsyncMongoClient: + if not h and not p: + client = AsyncMongoClient(**kwargs) + else: + client = AsyncMongoClient(h, p, **kwargs) + return client + + pytest_collection_modifyitems = pytest_conf.pytest_collection_modifyitems diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py new file mode 100644 index 0000000000..ac5ec3151e --- /dev/null +++ b/test/asynchronous/test_client_pytest.py @@ -0,0 +1,674 @@ +# Copyright 2013-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test the mongo_client module.""" +from __future__ import annotations + +import _thread as thread +import asyncio +import base64 +import contextlib +import copy +import datetime +import gc +import logging +import os +import re +import signal +import socket +import struct +import subprocess +import sys +import threading +import time +import uuid +from typing import Any, Iterable, Type, no_type_check +from unittest import mock +from unittest.mock import patch + +import pytest +import pytest_asyncio +from pymongo.lock import _async_create_lock + +from bson.binary import CSHARP_LEGACY, JAVA_LEGACY, PYTHON_LEGACY, Binary, UuidRepresentation +from pymongo.operations import _Op +from test.asynchronous.conftest import async_rs_or_single_client, simple_client, async_single_client + +sys.path[0:0] = [""] + +from test.asynchronous import ( + HAVE_IPADDRESS, + AsyncIntegrationTest, + AsyncMockClientTest, + AsyncUnitTest, + SkipTest, + client_knobs, + connected, + db_pwd, + db_user, + remove_all_users, + unittest, AsyncClientContext, +) +from test.asynchronous.pymongo_mocks import AsyncMockClient +from test.test_binary import BinaryData +from test.utils import ( + NTHREADS, + CMAPListener, + FunctionCallRecorder, + async_get_pool, + async_wait_until, + asyncAssertRaisesExactly, + delay, + gevent_monkey_patched, + is_greenthread_patched, + lazy_client_trial, + one, +) + +import bson +import pymongo +from bson import encode +from bson.codec_options import ( + CodecOptions, + DatetimeConversion, + TypeEncoder, + TypeRegistry, +) +from bson.son import SON +from bson.tz_util import utc +from pymongo import event_loggers, message, monitoring +from pymongo.asynchronous.command_cursor import AsyncCommandCursor +from pymongo.asynchronous.cursor import AsyncCursor, CursorType +from pymongo.asynchronous.database import AsyncDatabase +from pymongo.asynchronous.helpers import anext +from pymongo.asynchronous.mongo_client import AsyncMongoClient +from pymongo.asynchronous.pool import ( + AsyncConnection, +) +from pymongo.asynchronous.settings import TOPOLOGY_TYPE +from pymongo.asynchronous.topology import _ErrorContext +from pymongo.client_options import ClientOptions +from pymongo.common import _UUID_REPRESENTATIONS, CONNECT_TIMEOUT, MIN_SUPPORTED_WIRE_VERSION, has_c +from pymongo.compression_support import _have_snappy, _have_zstd +from pymongo.driver_info import DriverInfo +from pymongo.errors import ( + AutoReconnect, + ConfigurationError, + ConnectionFailure, + InvalidName, + InvalidOperation, + InvalidURI, + NetworkTimeout, + OperationFailure, + ServerSelectionTimeoutError, + WriteConcernError, +) +from pymongo.monitoring import ServerHeartbeatListener, ServerHeartbeatStartedEvent +from pymongo.pool_options import _MAX_METADATA_SIZE, _METADATA, ENV_VAR_K8S, PoolOptions +from pymongo.read_preferences import ReadPreference +from pymongo.server_description import ServerDescription +from pymongo.server_selectors import readable_server_selector, writable_server_selector +from pymongo.server_type import SERVER_TYPE +from pymongo.topology_description import TopologyDescription +from pymongo.write_concern import WriteConcern + +_IS_SYNC = False + +pytestmark = pytest.mark.asyncio(loop_scope="session") + +@pytest_asyncio.fixture(loop_scope="session") +async def async_client(async_client_context_fixture) -> AsyncMongoClient: + client = await async_rs_or_single_client(async_client_context_fixture, + connect=False, serverSelectionTimeoutMS=100 + ) + yield client + await client.close() + + +async def test_keyword_arg_defaults(): + client = simple_client( + socketTimeoutMS=None, + connectTimeoutMS=20000, + waitQueueTimeoutMS=None, + replicaSet=None, + read_preference=ReadPreference.PRIMARY, + ssl=False, + tlsCertificateKeyFile=None, + tlsAllowInvalidCertificates=True, + tlsCAFile=None, + connect=False, + serverSelectionTimeoutMS=12000, + ) + + options = client.options + pool_opts = options.pool_options + assert pool_opts.socket_timeout is None + # socket.Socket.settimeout takes a float in seconds + assert 20.0 == pool_opts.connect_timeout + assert pool_opts.wait_queue_timeout is None + assert pool_opts._ssl_context is None + assert options.replica_set_name is None + assert client.read_preference == ReadPreference.PRIMARY + assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 + +async def test_connect_timeout(): + client = simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) + pool_opts = client.options.pool_options + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + + client = simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) + pool_opts = client.options.pool_options + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + + client = simple_client( + "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False + ) + pool_opts = client.options.pool_options + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + +async def test_types(): + with pytest.raises(TypeError): + AsyncMongoClient(1) + with pytest.raises(TypeError): + AsyncMongoClient(1.14) + with pytest.raises(TypeError): + AsyncMongoClient("localhost", "27017") + with pytest.raises(TypeError): + AsyncMongoClient("localhost", 1.14) + with pytest.raises(TypeError): + AsyncMongoClient("localhost", []) + + with pytest.raises(ConfigurationError): + AsyncMongoClient([]) + +async def test_max_pool_size_zero(): + simple_client(maxPoolSize=0) + +async def test_uri_detection(): + with pytest.raises(ConfigurationError): + AsyncMongoClient("/foo") + with pytest.raises(ConfigurationError): + AsyncMongoClient("://") + with pytest.raises(ConfigurationError): + AsyncMongoClient("foo/") + + +async def test_get_db(async_client): + def make_db(base, name): + return base[name] + + with pytest.raises(InvalidName): + make_db(async_client, "") + with pytest.raises(InvalidName): + make_db(async_client, "te$t") + with pytest.raises(InvalidName): + make_db(async_client, "te.t") + with pytest.raises(InvalidName): + make_db(async_client, "te\\t") + with pytest.raises(InvalidName): + make_db(async_client, "te/t") + with pytest.raises(InvalidName): + make_db(async_client, "te st") + # Type and equality assertions + assert isinstance(async_client.test, AsyncDatabase) + assert async_client.test == async_client["test"] + assert async_client.test == AsyncDatabase(async_client, "test") + +async def test_get_database(async_client): + codec_options = CodecOptions(tz_aware=True) + write_concern = WriteConcern(w=2, j=True) + db = async_client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) + assert db.name == "foo" + assert db.codec_options == codec_options + assert db.read_preference == ReadPreference.SECONDARY + assert db.write_concern == write_concern + +async def test_getattr(async_client): + assert isinstance(async_client["_does_not_exist"], AsyncDatabase) + + with pytest.raises(AttributeError) as context: + async_client.client._does_not_exist + + # Message should be: + # "AttributeError: AsyncMongoClient has no attribute '_does_not_exist'. To + # access the _does_not_exist database, use client['_does_not_exist']". + assert "has no attribute '_does_not_exist'" in str(context.value) + + +async def test_iteration(async_client): + if _IS_SYNC: + msg = "'AsyncMongoClient' object is not iterable" + else: + msg = "'AsyncMongoClient' object is not an async iterator" + + with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + for _ in async_client: + break + + # Index fails + with pytest.raises(TypeError): + _ = async_client[0] + + # 'next' function fails + with pytest.raises(TypeError, match=msg): + _ = await anext(async_client) + + # 'next()' method fails + with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + _ = await async_client.anext() + + # Do not implement typing.Iterable + assert not isinstance(async_client, Iterable) + + + +async def test_get_default_database(async_client_context_fixture): + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/foo" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + assert AsyncDatabase(c, "foo") == c.get_default_database() + # Test that default doesn't override the URI value. + assert AsyncDatabase(c, "foo") == c.get_default_database("bar") + codec_options = CodecOptions(tz_aware=True) + write_concern = WriteConcern(w=2, j=True) + db = c.get_default_database(None, codec_options, ReadPreference.SECONDARY, write_concern) + assert "foo" == db.name + assert codec_options == db.codec_options + assert ReadPreference.SECONDARY == db.read_preference + assert write_concern == db.write_concern + + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + assert AsyncDatabase(c, "foo") == c.get_default_database("foo") + + +async def test_get_default_database_error(async_client_context_fixture): + # URI with no database. + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + with pytest.raises(ConfigurationError): + c.get_default_database() + +async def test_get_default_database_with_authsource(async_client_context_fixture): + # Ensure we distinguish database name from authSource. + uri = "mongodb://%s:%d/foo?authSource=src" % ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) + assert (AsyncDatabase(c, "foo") == c.get_default_database()) + +async def test_get_database_default(async_client_context_fixture): + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/foo" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + assert AsyncDatabase(c, "foo") == c.get_database() + +async def test_get_database_default_error(async_client_context_fixture): + # URI with no database. + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + with pytest.raises(ConfigurationError): + c.get_database() + +async def test_get_database_default_with_authsource(async_client_context_fixture): + # Ensure we distinguish database name from authSource. + uri = "mongodb://%s:%d/foo?authSource=src" % ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) + assert AsyncDatabase(c, "foo") == c.get_database() + +async def test_primary_read_pref_with_tags(async_client_context_fixture): + # No tags allowed with "primary". + with pytest.raises(ConfigurationError): + async with await async_single_client(async_client_context_fixture, "mongodb://host/?readpreferencetags=dc:east"): + pass + with pytest.raises(ConfigurationError): + async with await async_single_client(async_client_context_fixture, + "mongodb://host/?readpreference=primary&readpreferencetags=dc:east" + ): + pass + +async def test_read_preference(async_client_context_fixture): + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode + ) + assert c.read_preference == ReadPreference.NEAREST + +# async def test_metadata(): +# metadata = copy.deepcopy(_METADATA) +# if has_c(): +# metadata["driver"]["name"] = "PyMongo|c|async" +# else: +# metadata["driver"]["name"] = "PyMongo|async" +# metadata["application"] = {"name": "foobar"} +# client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") +# options = client.options +# self.assertEqual(options.pool_options.metadata, metadata) +# client = self.simple_client("foo", 27017, appname="foobar", connect=False) +# options = client.options +# self.assertEqual(options.pool_options.metadata, metadata) +# # No error +# self.simple_client(appname="x" * 128) +# with self.assertRaises(ValueError): +# self.simple_client(appname="x" * 129) +# # Bad "driver" options. +# self.assertRaises(TypeError, DriverInfo, "Foo", 1, "a") +# self.assertRaises(TypeError, DriverInfo, version="1", platform="a") +# self.assertRaises(TypeError, DriverInfo) +# with self.assertRaises(TypeError): +# self.simple_client(driver=1) +# with self.assertRaises(TypeError): +# self.simple_client(driver="abc") +# with self.assertRaises(TypeError): +# self.simple_client(driver=("Foo", "1", "a")) +# # Test appending to driver info. +# if has_c(): +# metadata["driver"]["name"] = "PyMongo|c|async|FooDriver" +# else: +# metadata["driver"]["name"] = "PyMongo|async|FooDriver" +# metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) +# client = self.simple_client( +# "foo", +# 27017, +# appname="foobar", +# driver=DriverInfo("FooDriver", "1.2.3", None), +# connect=False, +# ) +# options = client.options +# self.assertEqual(options.pool_options.metadata, metadata) +# metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) +# client = self.simple_client( +# "foo", +# 27017, +# appname="foobar", +# driver=DriverInfo("FooDriver", "1.2.3", "FooPlatform"), +# connect=False, +# ) +# options = client.options +# self.assertEqual(options.pool_options.metadata, metadata) +# # Test truncating driver info metadata. +# client = self.simple_client( +# driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), +# connect=False, +# ) +# options = client.options +# self.assertLessEqual( +# len(bson.encode(options.pool_options.metadata)), +# _MAX_METADATA_SIZE, +# ) +# client = self.simple_client( +# driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), +# connect=False, +# ) +# options = client.options +# self.assertLessEqual( +# len(bson.encode(options.pool_options.metadata)), +# _MAX_METADATA_SIZE, +# ) +# +# @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) +# def test_container_metadata(self): +# metadata = copy.deepcopy(_METADATA) +# metadata["driver"]["name"] = "PyMongo|async" +# metadata["env"] = {} +# metadata["env"]["container"] = {"orchestrator": "kubernetes"} +# client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") +# options = client.options +# self.assertEqual(options.pool_options.metadata["env"], metadata["env"]) + # + # async def test_kwargs_codec_options(self): + # class MyFloatType: + # def __init__(self, x): + # self.__x = x + # + # @property + # def x(self): + # return self.__x + # + # class MyFloatAsIntEncoder(TypeEncoder): + # python_type = MyFloatType + # + # def transform_python(self, value): + # return int(value) + # + # # Ensure codec options are passed in correctly + # document_class: Type[SON] = SON + # type_registry = TypeRegistry([MyFloatAsIntEncoder()]) + # tz_aware = True + # uuid_representation_label = "javaLegacy" + # unicode_decode_error_handler = "ignore" + # tzinfo = utc + # c = self.simple_client( + # document_class=document_class, + # type_registry=type_registry, + # tz_aware=tz_aware, + # uuidrepresentation=uuid_representation_label, + # unicode_decode_error_handler=unicode_decode_error_handler, + # tzinfo=tzinfo, + # connect=False, + # ) + # self.assertEqual(c.codec_options.document_class, document_class) + # self.assertEqual(c.codec_options.type_registry, type_registry) + # self.assertEqual(c.codec_options.tz_aware, tz_aware) + # self.assertEqual( + # c.codec_options.uuid_representation, + # _UUID_REPRESENTATIONS[uuid_representation_label], + # ) + # self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) + # self.assertEqual(c.codec_options.tzinfo, tzinfo) + # + # async def test_uri_codec_options(self): + # # Ensure codec options are passed in correctly + # uuid_representation_label = "javaLegacy" + # unicode_decode_error_handler = "ignore" + # datetime_conversion = "DATETIME_CLAMP" + # uri = ( + # "mongodb://%s:%d/foo?tz_aware=true&uuidrepresentation=" + # "%s&unicode_decode_error_handler=%s" + # "&datetime_conversion=%s" + # % ( + # await async_client_context.host, + # await async_client_context.port, + # uuid_representation_label, + # unicode_decode_error_handler, + # datetime_conversion, + # ) + # ) + # c = self.simple_client(uri, connect=False) + # self.assertEqual(c.codec_options.tz_aware, True) + # self.assertEqual( + # c.codec_options.uuid_representation, + # _UUID_REPRESENTATIONS[uuid_representation_label], + # ) + # self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) + # self.assertEqual( + # c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] + # ) + # + # # Change the passed datetime_conversion to a number and re-assert. + # uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") + # c = self.simple_client(uri, connect=False) + # self.assertEqual( + # c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] + # ) + # + # async def test_uri_option_precedence(self): + # # Ensure kwarg options override connection string options. + # uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" + # c = self.simple_client( + # uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" + # ) + # clopts = c.options + # opts = clopts._options + # + # self.assertEqual(opts["tls"], False) + # self.assertEqual(clopts.replica_set_name, "newname") + # self.assertEqual(clopts.read_preference, ReadPreference.SECONDARY_PREFERRED) + # + # async def test_connection_timeout_ms_propagates_to_DNS_resolver(self): + # # Patch the resolver. + # from pymongo.srv_resolver import _resolve + # + # patched_resolver = FunctionCallRecorder(_resolve) + # pymongo.srv_resolver._resolve = patched_resolver + # + # def reset_resolver(): + # pymongo.srv_resolver._resolve = _resolve + # + # self.addCleanup(reset_resolver) + # + # # Setup. + # base_uri = "mongodb+srv://test5.test.build.10gen.cc" + # connectTimeoutMS = 5000 + # expected_kw_value = 5.0 + # uri_with_timeout = base_uri + "/?connectTimeoutMS=6000" + # expected_uri_value = 6.0 + # + # async def test_scenario(args, kwargs, expected_value): + # patched_resolver.reset() + # self.simple_client(*args, **kwargs) + # for _, kw in patched_resolver.call_list(): + # self.assertAlmostEqual(kw["lifetime"], expected_value) + # + # # No timeout specified. + # await test_scenario((base_uri,), {}, CONNECT_TIMEOUT) + # + # # Timeout only specified in connection string. + # await test_scenario((uri_with_timeout,), {}, expected_uri_value) + # + # # Timeout only specified in keyword arguments. + # kwarg = {"connectTimeoutMS": connectTimeoutMS} + # await test_scenario((base_uri,), kwarg, expected_kw_value) + # + # # Timeout specified in both kwargs and connection string. + # await test_scenario((uri_with_timeout,), kwarg, expected_kw_value) + # + # async def test_uri_security_options(self): + # # Ensure that we don't silently override security-related options. + # with self.assertRaises(InvalidURI): + # self.simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) + # + # # Matching SSL and TLS options should not cause errors. + # c = self.simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) + # self.assertEqual(c.options._options["tls"], False) + # + # # Conflicting tlsInsecure options should raise an error. + # with self.assertRaises(InvalidURI): + # self.simple_client( + # "mongodb://localhost/?tlsInsecure=true", + # connect=False, + # tlsAllowInvalidHostnames=True, + # ) + # + # # Conflicting legacy tlsInsecure options should also raise an error. + # with self.assertRaises(InvalidURI): + # self.simple_client( + # "mongodb://localhost/?tlsInsecure=true", + # connect=False, + # tlsAllowInvalidCertificates=False, + # ) + # + # # Conflicting kwargs should raise InvalidURI + # with self.assertRaises(InvalidURI): + # self.simple_client(ssl=True, tls=False) + # + # async def test_event_listeners(self): + # c = self.simple_client(event_listeners=[], connect=False) + # self.assertEqual(c.options.event_listeners, []) + # listeners = [ + # event_loggers.CommandLogger(), + # event_loggers.HeartbeatLogger(), + # event_loggers.ServerLogger(), + # event_loggers.TopologyLogger(), + # event_loggers.ConnectionPoolLogger(), + # ] + # c = self.simple_client(event_listeners=listeners, connect=False) + # self.assertEqual(c.options.event_listeners, listeners) + # + # async def test_client_options(self): + # c = self.simple_client(connect=False) + # self.assertIsInstance(c.options, ClientOptions) + # self.assertIsInstance(c.options.pool_options, PoolOptions) + # self.assertEqual(c.options.server_selection_timeout, 30) + # self.assertEqual(c.options.pool_options.max_idle_time_seconds, None) + # self.assertIsInstance(c.options.retry_writes, bool) + # self.assertIsInstance(c.options.retry_reads, bool) + # + # def test_validate_suggestion(self): + # """Validate kwargs in constructor.""" + # for typo in ["auth", "Auth", "AUTH"]: + # expected = f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." + # expected = re.escape(expected) + # with self.assertRaisesRegex(ConfigurationError, expected): + # AsyncMongoClient(**{typo: "standard"}) # type: ignore[arg-type] + # + # @patch("pymongo.srv_resolver._SrvResolver.get_hosts") + # def test_detected_environment_logging(self, mock_get_hosts): + # normal_hosts = [ + # "normal.host.com", + # "host.cosmos.azure.com", + # "host.docdb.amazonaws.com", + # "host.docdb-elastic.amazonaws.com", + # ] + # srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] + # multi_host = ( + # "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" + # ) + # with self.assertLogs("pymongo", level="INFO") as cm: + # for host in normal_hosts: + # AsyncMongoClient(host, connect=False) + # for host in srv_hosts: + # mock_get_hosts.return_value = [(host, 1)] + # AsyncMongoClient(host, connect=False) + # AsyncMongoClient(multi_host, connect=False) + # logs = [record.getMessage() for record in cm.records if record.name == "pymongo.client"] + # self.assertEqual(len(logs), 7) + # + # @patch("pymongo.srv_resolver._SrvResolver.get_hosts") + # async def test_detected_environment_warning(self, mock_get_hosts): + # with self._caplog.at_level(logging.WARN): + # normal_hosts = [ + # "host.cosmos.azure.com", + # "host.docdb.amazonaws.com", + # "host.docdb-elastic.amazonaws.com", + # ] + # srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] + # multi_host = ( + # "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" + # ) + # for host in normal_hosts: + # with self.assertWarns(UserWarning): + # self.simple_client(host) + # for host in srv_hosts: + # mock_get_hosts.return_value = [(host, 1)] + # with self.assertWarns(UserWarning): + # self.simple_client(host) + # with self.assertWarns(UserWarning): + # self.simple_client(multi_host) diff --git a/test/pytest_conf.py b/test/pytest_conf.py index a6e24cd9b1..1a198956a5 100644 --- a/test/pytest_conf.py +++ b/test/pytest_conf.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + def pytest_collection_modifyitems(items, config): # Markers that should overlap with the default markers. @@ -10,6 +12,6 @@ def pytest_collection_modifyitems(items, config): default_marker = "default_async" else: default_marker = "default" - markers = [m for m in item.iter_markers() if m not in overlap_markers] + markers = [m for m in item.iter_markers() if m.name not in overlap_markers] if not markers: item.add_marker(default_marker) From b6baf798f7896393ca92375c769ca4bb874db637 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 21 Jan 2025 12:55:53 -0500 Subject: [PATCH 05/29] TestAsyncClientUnitTest done --- test/asynchronous/conftest.py | 12 + test/asynchronous/test_client_pytest.py | 1079 +++++++++++------------ 2 files changed, 546 insertions(+), 545 deletions(-) diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index ab3c3e7bc0..7b1df58357 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -3,6 +3,7 @@ import asyncio import sys +import pymongo from typing_extensions import Any from pymongo import AsyncMongoClient @@ -14,6 +15,8 @@ import pytest import pytest_asyncio +from test.utils import FunctionCallRecorder + _IS_SYNC = False @@ -124,6 +127,15 @@ def simple_client(h: Any = None, p: Any = None, **kwargs: Any) -> AsyncMongoClie client = AsyncMongoClient(h, p, **kwargs) return client +@pytest.fixture(scope="function") +def patch_resolver(): + from pymongo.srv_resolver import _resolve + + patched_resolver = FunctionCallRecorder(_resolve) + pymongo.srv_resolver._resolve = patched_resolver + yield patched_resolver + pymongo.srv_resolver._resolve = _resolve + pytest_collection_modifyitems = pytest_conf.pytest_collection_modifyitems diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py index ac5ec3151e..adc03f4249 100644 --- a/test/asynchronous/test_client_pytest.py +++ b/test/asynchronous/test_client_pytest.py @@ -127,548 +127,537 @@ pytestmark = pytest.mark.asyncio(loop_scope="session") -@pytest_asyncio.fixture(loop_scope="session") -async def async_client(async_client_context_fixture) -> AsyncMongoClient: - client = await async_rs_or_single_client(async_client_context_fixture, - connect=False, serverSelectionTimeoutMS=100 - ) - yield client - await client.close() - - -async def test_keyword_arg_defaults(): - client = simple_client( - socketTimeoutMS=None, - connectTimeoutMS=20000, - waitQueueTimeoutMS=None, - replicaSet=None, - read_preference=ReadPreference.PRIMARY, - ssl=False, - tlsCertificateKeyFile=None, - tlsAllowInvalidCertificates=True, - tlsCAFile=None, - connect=False, - serverSelectionTimeoutMS=12000, - ) - - options = client.options - pool_opts = options.pool_options - assert pool_opts.socket_timeout is None - # socket.Socket.settimeout takes a float in seconds - assert 20.0 == pool_opts.connect_timeout - assert pool_opts.wait_queue_timeout is None - assert pool_opts._ssl_context is None - assert options.replica_set_name is None - assert client.read_preference == ReadPreference.PRIMARY - assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 - -async def test_connect_timeout(): - client = simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) - pool_opts = client.options.pool_options - assert pool_opts.socket_timeout is None - assert pool_opts.connect_timeout is None - - client = simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) - pool_opts = client.options.pool_options - assert pool_opts.socket_timeout is None - assert pool_opts.connect_timeout is None - - client = simple_client( - "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False - ) - pool_opts = client.options.pool_options - assert pool_opts.socket_timeout is None - assert pool_opts.connect_timeout is None - -async def test_types(): - with pytest.raises(TypeError): - AsyncMongoClient(1) - with pytest.raises(TypeError): - AsyncMongoClient(1.14) - with pytest.raises(TypeError): - AsyncMongoClient("localhost", "27017") - with pytest.raises(TypeError): - AsyncMongoClient("localhost", 1.14) - with pytest.raises(TypeError): - AsyncMongoClient("localhost", []) - - with pytest.raises(ConfigurationError): - AsyncMongoClient([]) - -async def test_max_pool_size_zero(): - simple_client(maxPoolSize=0) - -async def test_uri_detection(): - with pytest.raises(ConfigurationError): - AsyncMongoClient("/foo") - with pytest.raises(ConfigurationError): - AsyncMongoClient("://") - with pytest.raises(ConfigurationError): - AsyncMongoClient("foo/") - - -async def test_get_db(async_client): - def make_db(base, name): - return base[name] - - with pytest.raises(InvalidName): - make_db(async_client, "") - with pytest.raises(InvalidName): - make_db(async_client, "te$t") - with pytest.raises(InvalidName): - make_db(async_client, "te.t") - with pytest.raises(InvalidName): - make_db(async_client, "te\\t") - with pytest.raises(InvalidName): - make_db(async_client, "te/t") - with pytest.raises(InvalidName): - make_db(async_client, "te st") - # Type and equality assertions - assert isinstance(async_client.test, AsyncDatabase) - assert async_client.test == async_client["test"] - assert async_client.test == AsyncDatabase(async_client, "test") - -async def test_get_database(async_client): - codec_options = CodecOptions(tz_aware=True) - write_concern = WriteConcern(w=2, j=True) - db = async_client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) - assert db.name == "foo" - assert db.codec_options == codec_options - assert db.read_preference == ReadPreference.SECONDARY - assert db.write_concern == write_concern - -async def test_getattr(async_client): - assert isinstance(async_client["_does_not_exist"], AsyncDatabase) - - with pytest.raises(AttributeError) as context: - async_client.client._does_not_exist - - # Message should be: - # "AttributeError: AsyncMongoClient has no attribute '_does_not_exist'. To - # access the _does_not_exist database, use client['_does_not_exist']". - assert "has no attribute '_does_not_exist'" in str(context.value) - - -async def test_iteration(async_client): - if _IS_SYNC: - msg = "'AsyncMongoClient' object is not iterable" - else: - msg = "'AsyncMongoClient' object is not an async iterator" - - with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): - for _ in async_client: - break - - # Index fails - with pytest.raises(TypeError): - _ = async_client[0] - - # 'next' function fails - with pytest.raises(TypeError, match=msg): - _ = await anext(async_client) - - # 'next()' method fails - with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): - _ = await async_client.anext() - - # Do not implement typing.Iterable - assert not isinstance(async_client, Iterable) - - - -async def test_get_default_database(async_client_context_fixture): - c = await async_rs_or_single_client(async_client_context_fixture, - "mongodb://%s:%d/foo" - % (await async_client_context_fixture.host, await async_client_context_fixture.port), - connect=False, - ) - assert AsyncDatabase(c, "foo") == c.get_default_database() - # Test that default doesn't override the URI value. - assert AsyncDatabase(c, "foo") == c.get_default_database("bar") - codec_options = CodecOptions(tz_aware=True) - write_concern = WriteConcern(w=2, j=True) - db = c.get_default_database(None, codec_options, ReadPreference.SECONDARY, write_concern) - assert "foo" == db.name - assert codec_options == db.codec_options - assert ReadPreference.SECONDARY == db.read_preference - assert write_concern == db.write_concern - - c = await async_rs_or_single_client(async_client_context_fixture, - "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), - connect=False, - ) - assert AsyncDatabase(c, "foo") == c.get_default_database("foo") - - -async def test_get_default_database_error(async_client_context_fixture): - # URI with no database. - c = await async_rs_or_single_client(async_client_context_fixture, - "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), - connect=False, - ) - with pytest.raises(ConfigurationError): - c.get_default_database() - -async def test_get_default_database_with_authsource(async_client_context_fixture): - # Ensure we distinguish database name from authSource. - uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context_fixture.host, - await async_client_context_fixture.port, - ) - c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) - assert (AsyncDatabase(c, "foo") == c.get_default_database()) - -async def test_get_database_default(async_client_context_fixture): - c = await async_rs_or_single_client(async_client_context_fixture, - "mongodb://%s:%d/foo" - % (await async_client_context_fixture.host, await async_client_context_fixture.port), - connect=False, - ) - assert AsyncDatabase(c, "foo") == c.get_database() - -async def test_get_database_default_error(async_client_context_fixture): - # URI with no database. - c = await async_rs_or_single_client(async_client_context_fixture, - "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), - connect=False, - ) - with pytest.raises(ConfigurationError): - c.get_database() - -async def test_get_database_default_with_authsource(async_client_context_fixture): - # Ensure we distinguish database name from authSource. - uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context_fixture.host, - await async_client_context_fixture.port, - ) - c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) - assert AsyncDatabase(c, "foo") == c.get_database() - -async def test_primary_read_pref_with_tags(async_client_context_fixture): - # No tags allowed with "primary". - with pytest.raises(ConfigurationError): - async with await async_single_client(async_client_context_fixture, "mongodb://host/?readpreferencetags=dc:east"): - pass - with pytest.raises(ConfigurationError): - async with await async_single_client(async_client_context_fixture, - "mongodb://host/?readpreference=primary&readpreferencetags=dc:east" - ): - pass - -async def test_read_preference(async_client_context_fixture): - c = await async_rs_or_single_client(async_client_context_fixture, - "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode - ) - assert c.read_preference == ReadPreference.NEAREST - -# async def test_metadata(): -# metadata = copy.deepcopy(_METADATA) -# if has_c(): -# metadata["driver"]["name"] = "PyMongo|c|async" -# else: -# metadata["driver"]["name"] = "PyMongo|async" -# metadata["application"] = {"name": "foobar"} -# client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") -# options = client.options -# self.assertEqual(options.pool_options.metadata, metadata) -# client = self.simple_client("foo", 27017, appname="foobar", connect=False) -# options = client.options -# self.assertEqual(options.pool_options.metadata, metadata) -# # No error -# self.simple_client(appname="x" * 128) -# with self.assertRaises(ValueError): -# self.simple_client(appname="x" * 129) -# # Bad "driver" options. -# self.assertRaises(TypeError, DriverInfo, "Foo", 1, "a") -# self.assertRaises(TypeError, DriverInfo, version="1", platform="a") -# self.assertRaises(TypeError, DriverInfo) -# with self.assertRaises(TypeError): -# self.simple_client(driver=1) -# with self.assertRaises(TypeError): -# self.simple_client(driver="abc") -# with self.assertRaises(TypeError): -# self.simple_client(driver=("Foo", "1", "a")) -# # Test appending to driver info. -# if has_c(): -# metadata["driver"]["name"] = "PyMongo|c|async|FooDriver" -# else: -# metadata["driver"]["name"] = "PyMongo|async|FooDriver" -# metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) -# client = self.simple_client( -# "foo", -# 27017, -# appname="foobar", -# driver=DriverInfo("FooDriver", "1.2.3", None), -# connect=False, -# ) -# options = client.options -# self.assertEqual(options.pool_options.metadata, metadata) -# metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) -# client = self.simple_client( -# "foo", -# 27017, -# appname="foobar", -# driver=DriverInfo("FooDriver", "1.2.3", "FooPlatform"), -# connect=False, -# ) -# options = client.options -# self.assertEqual(options.pool_options.metadata, metadata) -# # Test truncating driver info metadata. -# client = self.simple_client( -# driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), -# connect=False, -# ) -# options = client.options -# self.assertLessEqual( -# len(bson.encode(options.pool_options.metadata)), -# _MAX_METADATA_SIZE, -# ) -# client = self.simple_client( -# driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), -# connect=False, -# ) -# options = client.options -# self.assertLessEqual( -# len(bson.encode(options.pool_options.metadata)), -# _MAX_METADATA_SIZE, -# ) -# -# @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) -# def test_container_metadata(self): -# metadata = copy.deepcopy(_METADATA) -# metadata["driver"]["name"] = "PyMongo|async" -# metadata["env"] = {} -# metadata["env"]["container"] = {"orchestrator": "kubernetes"} -# client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") -# options = client.options -# self.assertEqual(options.pool_options.metadata["env"], metadata["env"]) - # - # async def test_kwargs_codec_options(self): - # class MyFloatType: - # def __init__(self, x): - # self.__x = x - # - # @property - # def x(self): - # return self.__x - # - # class MyFloatAsIntEncoder(TypeEncoder): - # python_type = MyFloatType - # - # def transform_python(self, value): - # return int(value) - # - # # Ensure codec options are passed in correctly - # document_class: Type[SON] = SON - # type_registry = TypeRegistry([MyFloatAsIntEncoder()]) - # tz_aware = True - # uuid_representation_label = "javaLegacy" - # unicode_decode_error_handler = "ignore" - # tzinfo = utc - # c = self.simple_client( - # document_class=document_class, - # type_registry=type_registry, - # tz_aware=tz_aware, - # uuidrepresentation=uuid_representation_label, - # unicode_decode_error_handler=unicode_decode_error_handler, - # tzinfo=tzinfo, - # connect=False, - # ) - # self.assertEqual(c.codec_options.document_class, document_class) - # self.assertEqual(c.codec_options.type_registry, type_registry) - # self.assertEqual(c.codec_options.tz_aware, tz_aware) - # self.assertEqual( - # c.codec_options.uuid_representation, - # _UUID_REPRESENTATIONS[uuid_representation_label], - # ) - # self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) - # self.assertEqual(c.codec_options.tzinfo, tzinfo) - # - # async def test_uri_codec_options(self): - # # Ensure codec options are passed in correctly - # uuid_representation_label = "javaLegacy" - # unicode_decode_error_handler = "ignore" - # datetime_conversion = "DATETIME_CLAMP" - # uri = ( - # "mongodb://%s:%d/foo?tz_aware=true&uuidrepresentation=" - # "%s&unicode_decode_error_handler=%s" - # "&datetime_conversion=%s" - # % ( - # await async_client_context.host, - # await async_client_context.port, - # uuid_representation_label, - # unicode_decode_error_handler, - # datetime_conversion, - # ) - # ) - # c = self.simple_client(uri, connect=False) - # self.assertEqual(c.codec_options.tz_aware, True) - # self.assertEqual( - # c.codec_options.uuid_representation, - # _UUID_REPRESENTATIONS[uuid_representation_label], - # ) - # self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) - # self.assertEqual( - # c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] - # ) - # - # # Change the passed datetime_conversion to a number and re-assert. - # uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") - # c = self.simple_client(uri, connect=False) - # self.assertEqual( - # c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] - # ) - # - # async def test_uri_option_precedence(self): - # # Ensure kwarg options override connection string options. - # uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" - # c = self.simple_client( - # uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" - # ) - # clopts = c.options - # opts = clopts._options - # - # self.assertEqual(opts["tls"], False) - # self.assertEqual(clopts.replica_set_name, "newname") - # self.assertEqual(clopts.read_preference, ReadPreference.SECONDARY_PREFERRED) - # - # async def test_connection_timeout_ms_propagates_to_DNS_resolver(self): - # # Patch the resolver. - # from pymongo.srv_resolver import _resolve - # - # patched_resolver = FunctionCallRecorder(_resolve) - # pymongo.srv_resolver._resolve = patched_resolver - # - # def reset_resolver(): - # pymongo.srv_resolver._resolve = _resolve - # - # self.addCleanup(reset_resolver) - # - # # Setup. - # base_uri = "mongodb+srv://test5.test.build.10gen.cc" - # connectTimeoutMS = 5000 - # expected_kw_value = 5.0 - # uri_with_timeout = base_uri + "/?connectTimeoutMS=6000" - # expected_uri_value = 6.0 - # - # async def test_scenario(args, kwargs, expected_value): - # patched_resolver.reset() - # self.simple_client(*args, **kwargs) - # for _, kw in patched_resolver.call_list(): - # self.assertAlmostEqual(kw["lifetime"], expected_value) - # - # # No timeout specified. - # await test_scenario((base_uri,), {}, CONNECT_TIMEOUT) - # - # # Timeout only specified in connection string. - # await test_scenario((uri_with_timeout,), {}, expected_uri_value) - # - # # Timeout only specified in keyword arguments. - # kwarg = {"connectTimeoutMS": connectTimeoutMS} - # await test_scenario((base_uri,), kwarg, expected_kw_value) - # - # # Timeout specified in both kwargs and connection string. - # await test_scenario((uri_with_timeout,), kwarg, expected_kw_value) - # - # async def test_uri_security_options(self): - # # Ensure that we don't silently override security-related options. - # with self.assertRaises(InvalidURI): - # self.simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) - # - # # Matching SSL and TLS options should not cause errors. - # c = self.simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) - # self.assertEqual(c.options._options["tls"], False) - # - # # Conflicting tlsInsecure options should raise an error. - # with self.assertRaises(InvalidURI): - # self.simple_client( - # "mongodb://localhost/?tlsInsecure=true", - # connect=False, - # tlsAllowInvalidHostnames=True, - # ) - # - # # Conflicting legacy tlsInsecure options should also raise an error. - # with self.assertRaises(InvalidURI): - # self.simple_client( - # "mongodb://localhost/?tlsInsecure=true", - # connect=False, - # tlsAllowInvalidCertificates=False, - # ) - # - # # Conflicting kwargs should raise InvalidURI - # with self.assertRaises(InvalidURI): - # self.simple_client(ssl=True, tls=False) - # - # async def test_event_listeners(self): - # c = self.simple_client(event_listeners=[], connect=False) - # self.assertEqual(c.options.event_listeners, []) - # listeners = [ - # event_loggers.CommandLogger(), - # event_loggers.HeartbeatLogger(), - # event_loggers.ServerLogger(), - # event_loggers.TopologyLogger(), - # event_loggers.ConnectionPoolLogger(), - # ] - # c = self.simple_client(event_listeners=listeners, connect=False) - # self.assertEqual(c.options.event_listeners, listeners) - # - # async def test_client_options(self): - # c = self.simple_client(connect=False) - # self.assertIsInstance(c.options, ClientOptions) - # self.assertIsInstance(c.options.pool_options, PoolOptions) - # self.assertEqual(c.options.server_selection_timeout, 30) - # self.assertEqual(c.options.pool_options.max_idle_time_seconds, None) - # self.assertIsInstance(c.options.retry_writes, bool) - # self.assertIsInstance(c.options.retry_reads, bool) - # - # def test_validate_suggestion(self): - # """Validate kwargs in constructor.""" - # for typo in ["auth", "Auth", "AUTH"]: - # expected = f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." - # expected = re.escape(expected) - # with self.assertRaisesRegex(ConfigurationError, expected): - # AsyncMongoClient(**{typo: "standard"}) # type: ignore[arg-type] - # - # @patch("pymongo.srv_resolver._SrvResolver.get_hosts") - # def test_detected_environment_logging(self, mock_get_hosts): - # normal_hosts = [ - # "normal.host.com", - # "host.cosmos.azure.com", - # "host.docdb.amazonaws.com", - # "host.docdb-elastic.amazonaws.com", - # ] - # srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] - # multi_host = ( - # "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" - # ) - # with self.assertLogs("pymongo", level="INFO") as cm: - # for host in normal_hosts: - # AsyncMongoClient(host, connect=False) - # for host in srv_hosts: - # mock_get_hosts.return_value = [(host, 1)] - # AsyncMongoClient(host, connect=False) - # AsyncMongoClient(multi_host, connect=False) - # logs = [record.getMessage() for record in cm.records if record.name == "pymongo.client"] - # self.assertEqual(len(logs), 7) - # - # @patch("pymongo.srv_resolver._SrvResolver.get_hosts") - # async def test_detected_environment_warning(self, mock_get_hosts): - # with self._caplog.at_level(logging.WARN): - # normal_hosts = [ - # "host.cosmos.azure.com", - # "host.docdb.amazonaws.com", - # "host.docdb-elastic.amazonaws.com", - # ] - # srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] - # multi_host = ( - # "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" - # ) - # for host in normal_hosts: - # with self.assertWarns(UserWarning): - # self.simple_client(host) - # for host in srv_hosts: - # mock_get_hosts.return_value = [(host, 1)] - # with self.assertWarns(UserWarning): - # self.simple_client(host) - # with self.assertWarns(UserWarning): - # self.simple_client(multi_host) + + +class TestAsyncClientUnitTest: + @pytest_asyncio.fixture(loop_scope="session") + async def async_client(self, async_client_context_fixture) -> AsyncMongoClient: + client = await async_rs_or_single_client(async_client_context_fixture, + connect=False, serverSelectionTimeoutMS=100 + ) + yield client + await client.close() + + async def test_keyword_arg_defaults(self): + client = simple_client( + socketTimeoutMS=None, + connectTimeoutMS=20000, + waitQueueTimeoutMS=None, + replicaSet=None, + read_preference=ReadPreference.PRIMARY, + ssl=False, + tlsCertificateKeyFile=None, + tlsAllowInvalidCertificates=True, + tlsCAFile=None, + connect=False, + serverSelectionTimeoutMS=12000, + ) + + options = client.options + pool_opts = options.pool_options + assert pool_opts.socket_timeout is None + # socket.Socket.settimeout takes a float in seconds + assert 20.0 == pool_opts.connect_timeout + assert pool_opts.wait_queue_timeout is None + assert pool_opts._ssl_context is None + assert options.replica_set_name is None + assert client.read_preference == ReadPreference.PRIMARY + assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 + + async def test_connect_timeout(self): + client = simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) + pool_opts = client.options.pool_options + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + + client = simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) + pool_opts = client.options.pool_options + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + + client = simple_client( + "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False + ) + pool_opts = client.options.pool_options + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + + async def test_types(self): + with pytest.raises(TypeError): + AsyncMongoClient(1) + with pytest.raises(TypeError): + AsyncMongoClient(1.14) + with pytest.raises(TypeError): + AsyncMongoClient("localhost", "27017") + with pytest.raises(TypeError): + AsyncMongoClient("localhost", 1.14) + with pytest.raises(TypeError): + AsyncMongoClient("localhost", []) + + with pytest.raises(ConfigurationError): + AsyncMongoClient([]) + + async def test_max_pool_size_zero(self): + simple_client(maxPoolSize=0) + + async def test_uri_detection(self): + with pytest.raises(ConfigurationError): + AsyncMongoClient("/foo") + with pytest.raises(ConfigurationError): + AsyncMongoClient("://") + with pytest.raises(ConfigurationError): + AsyncMongoClient("foo/") + + + async def test_get_db(self, async_client): + def make_db(base, name): + return base[name] + + with pytest.raises(InvalidName): + make_db(async_client, "") + with pytest.raises(InvalidName): + make_db(async_client, "te$t") + with pytest.raises(InvalidName): + make_db(async_client, "te.t") + with pytest.raises(InvalidName): + make_db(async_client, "te\\t") + with pytest.raises(InvalidName): + make_db(async_client, "te/t") + with pytest.raises(InvalidName): + make_db(async_client, "te st") + # Type and equality assertions + assert isinstance(async_client.test, AsyncDatabase) + assert async_client.test == async_client["test"] + assert async_client.test == AsyncDatabase(async_client, "test") + + async def test_get_database(self, async_client): + codec_options = CodecOptions(tz_aware=True) + write_concern = WriteConcern(w=2, j=True) + db = async_client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) + assert db.name == "foo" + assert db.codec_options == codec_options + assert db.read_preference == ReadPreference.SECONDARY + assert db.write_concern == write_concern + + async def test_getattr(self, async_client): + assert isinstance(async_client["_does_not_exist"], AsyncDatabase) + + with pytest.raises(AttributeError) as context: + async_client.client._does_not_exist + + # Message should be: + # "AttributeError: AsyncMongoClient has no attribute '_does_not_exist'. To + # access the _does_not_exist database, use client['_does_not_exist']". + assert "has no attribute '_does_not_exist'" in str(context.value) + + + async def test_iteration(self, async_client): + if _IS_SYNC: + msg = "'AsyncMongoClient' object is not iterable" + else: + msg = "'AsyncMongoClient' object is not an async iterator" + + with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + for _ in async_client: + break + + # Index fails + with pytest.raises(TypeError): + _ = async_client[0] + + # 'next' function fails + with pytest.raises(TypeError, match=msg): + _ = await anext(async_client) + + # 'next()' method fails + with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + _ = await async_client.anext() + + # Do not implement typing.Iterable + assert not isinstance(async_client, Iterable) + + + + async def test_get_default_database(self, async_client_context_fixture): + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/foo" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + assert AsyncDatabase(c, "foo") == c.get_default_database() + # Test that default doesn't override the URI value. + assert AsyncDatabase(c, "foo") == c.get_default_database("bar") + codec_options = CodecOptions(tz_aware=True) + write_concern = WriteConcern(w=2, j=True) + db = c.get_default_database(None, codec_options, ReadPreference.SECONDARY, write_concern) + assert "foo" == db.name + assert codec_options == db.codec_options + assert ReadPreference.SECONDARY == db.read_preference + assert write_concern == db.write_concern + + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + assert AsyncDatabase(c, "foo") == c.get_default_database("foo") + + + async def test_get_default_database_error(self, async_client_context_fixture): + # URI with no database. + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + with pytest.raises(ConfigurationError): + c.get_default_database() + + async def test_get_default_database_with_authsource(self, async_client_context_fixture): + # Ensure we distinguish database name from authSource. + uri = "mongodb://%s:%d/foo?authSource=src" % ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) + assert (AsyncDatabase(c, "foo") == c.get_default_database()) + + async def test_get_database_default(self, async_client_context_fixture): + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/foo" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + assert AsyncDatabase(c, "foo") == c.get_database() + + async def test_get_database_default_error(self, async_client_context_fixture): + # URI with no database. + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + connect=False, + ) + with pytest.raises(ConfigurationError): + c.get_database() + + async def test_get_database_default_with_authsource(self, async_client_context_fixture): + # Ensure we distinguish database name from authSource. + uri = "mongodb://%s:%d/foo?authSource=src" % ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) + assert AsyncDatabase(c, "foo") == c.get_database() + + async def test_primary_read_pref_with_tags(self, async_client_context_fixture): + # No tags allowed with "primary". + with pytest.raises(ConfigurationError): + async with await async_single_client(async_client_context_fixture, "mongodb://host/?readpreferencetags=dc:east"): + pass + with pytest.raises(ConfigurationError): + async with await async_single_client(async_client_context_fixture, + "mongodb://host/?readpreference=primary&readpreferencetags=dc:east" + ): + pass + + async def test_read_preference(self, async_client_context_fixture): + c = await async_rs_or_single_client(async_client_context_fixture, + "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode + ) + assert c.read_preference == ReadPreference.NEAREST + + async def test_metadata(self): + metadata = copy.deepcopy(_METADATA) + if has_c(): + metadata["driver"]["name"] = "PyMongo|c|async" + else: + metadata["driver"]["name"] = "PyMongo|async" + metadata["application"] = {"name": "foobar"} + + client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + options = client.options + assert options.pool_options.metadata == metadata + + client = simple_client("foo", 27017, appname="foobar", connect=False) + options = client.options + assert options.pool_options.metadata == metadata + + # No error + simple_client(appname="x" * 128) + with pytest.raises(ValueError): + simple_client(appname="x" * 129) + + # Bad "driver" options. + with pytest.raises(TypeError): + DriverInfo("Foo", 1, "a") + with pytest.raises(TypeError): + DriverInfo(version="1", platform="a") + with pytest.raises(TypeError): + DriverInfo() + with pytest.raises(TypeError): + simple_client(driver=1) + with pytest.raises(TypeError): + simple_client(driver="abc") + with pytest.raises(TypeError): + simple_client(driver=("Foo", "1", "a")) + + # Test appending to driver info. + if has_c(): + metadata["driver"]["name"] = "PyMongo|c|async|FooDriver" + else: + metadata["driver"]["name"] = "PyMongo|async|FooDriver" + metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) + + client = simple_client( + "foo", + 27017, + appname="foobar", + driver=DriverInfo("FooDriver", "1.2.3", None), + connect=False, + ) + options = client.options + assert options.pool_options.metadata == metadata + + metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) + client = simple_client( + "foo", + 27017, + appname="foobar", + driver=DriverInfo("FooDriver", "1.2.3", "FooPlatform"), + connect=False, + ) + options = client.options + assert options.pool_options.metadata == metadata + + # Test truncating driver info metadata. + client = simple_client( + driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), + connect=False, + ) + options = client.options + assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE + + client = simple_client( + driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), + connect=False, + ) + options = client.options + assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE + + + @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) + async def test_container_metadata(self): + metadata = copy.deepcopy(_METADATA) + metadata["driver"]["name"] = "PyMongo|async" + metadata["env"] = {} + metadata["env"]["container"] = {"orchestrator": "kubernetes"} + + client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + options = client.options + assert options.pool_options.metadata["env"] == metadata["env"] + + + async def test_kwargs_codec_options(self): + class MyFloatType: + def __init__(self, x): + self.__x = x + + @property + def x(self): + return self.__x + + class MyFloatAsIntEncoder(TypeEncoder): + python_type = MyFloatType + + def transform_python(self, value): + return int(value) + + # Ensure codec options are passed in correctly + document_class: Type[SON] = SON + type_registry = TypeRegistry([MyFloatAsIntEncoder()]) + tz_aware = True + uuid_representation_label = "javaLegacy" + unicode_decode_error_handler = "ignore" + tzinfo = utc + c = simple_client( + document_class=document_class, + type_registry=type_registry, + tz_aware=tz_aware, + uuidrepresentation=uuid_representation_label, + unicode_decode_error_handler=unicode_decode_error_handler, + tzinfo=tzinfo, + connect=False, + ) + assert c.codec_options.document_class == document_class + assert c.codec_options.type_registry == type_registry + assert c.codec_options.tz_aware == tz_aware + assert c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] + assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler + assert c.codec_options.tzinfo == tzinfo + + + async def test_uri_codec_options(self, async_client_context_fixture): + uuid_representation_label = "javaLegacy" + unicode_decode_error_handler = "ignore" + datetime_conversion = "DATETIME_CLAMP" + uri = ( + "mongodb://%s:%d/foo?tz_aware=true&uuidrepresentation=" + "%s&unicode_decode_error_handler=%s" + "&datetime_conversion=%s" + % ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + uuid_representation_label, + unicode_decode_error_handler, + datetime_conversion, + ) + ) + c = simple_client(uri, connect=False) + assert c.codec_options.tz_aware is True + assert c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] + assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler + assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] + # Change the passed datetime_conversion to a number and re-assert. + uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") + c = simple_client(uri, connect=False) + assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] + + async def test_uri_option_precedence(self): + # Ensure kwarg options override connection string options. + uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" + c = simple_client( + uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" + ) + clopts = c.options + opts = clopts._options + assert opts["tls"] is False + assert clopts.replica_set_name == "newname" + assert clopts.read_preference == ReadPreference.SECONDARY_PREFERRED + + async def test_connection_timeout_ms_propagates_to_DNS_resolver(self, patch_resolver): + base_uri = "mongodb+srv://test5.test.build.10gen.cc" + connectTimeoutMS = 5000 + expected_kw_value = 5.0 + uri_with_timeout = base_uri + "/?connectTimeoutMS=6000" + expected_uri_value = 6.0 + async def test_scenario(args, kwargs, expected_value): + patch_resolver.reset() + simple_client(*args, **kwargs) + for _, kw in patch_resolver.call_list(): + assert pytest.approx(kw["lifetime"], rel=1e-6) == expected_value + + # No timeout specified. + await test_scenario((base_uri,), {}, CONNECT_TIMEOUT) + + # Timeout only specified in connection string. + await test_scenario((uri_with_timeout,), {}, expected_uri_value) + + # Timeout only specified in keyword arguments. + kwarg = {"connectTimeoutMS": connectTimeoutMS} + await test_scenario((base_uri,), kwarg, expected_kw_value) + + # Timeout specified in both kwargs and connection string. + await test_scenario((uri_with_timeout,), kwarg, expected_kw_value) + + + async def test_uri_security_options(self): + # Ensure that we don't silently override security-related options. + with pytest.raises(InvalidURI): + simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) + + # Matching SSL and TLS options should not cause errors. + c = simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) + assert c.options._options["tls"] is False + + # Conflicting tlsInsecure options should raise an error. + with pytest.raises(InvalidURI): + simple_client( + "mongodb://localhost/?tlsInsecure=true", + connect=False, + tlsAllowInvalidHostnames=True, + ) + + # Conflicting legacy tlsInsecure options should also raise an error. + with pytest.raises(InvalidURI): + simple_client( + "mongodb://localhost/?tlsInsecure=true", + connect=False, + tlsAllowInvalidCertificates=False, + ) + + # Conflicting kwargs should raise InvalidURI + with pytest.raises(InvalidURI): + simple_client(ssl=True, tls=False) + + async def test_event_listeners(self): + c = simple_client(event_listeners=[], connect=False) + assert c.options.event_listeners == [] + listeners = [ + event_loggers.CommandLogger(), + event_loggers.HeartbeatLogger(), + event_loggers.ServerLogger(), + event_loggers.TopologyLogger(), + event_loggers.ConnectionPoolLogger(), + ] + c = simple_client(event_listeners=listeners, connect=False) + assert c.options.event_listeners == listeners + + async def test_client_options(self): + c = simple_client(connect=False) + assert isinstance(c.options, ClientOptions) + assert isinstance(c.options.pool_options, PoolOptions) + assert c.options.server_selection_timeout == 30 + assert c.options.pool_options.max_idle_time_seconds is None + assert isinstance(c.options.retry_writes, bool) + assert isinstance(c.options.retry_reads, bool) + + async def test_validate_suggestion(self): + """Validate kwargs in constructor.""" + for typo in ["auth", "Auth", "AUTH"]: + expected = ( + f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, " + f"authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." + ) + expected = re.escape(expected) + with pytest.raises(ConfigurationError, match=expected): + AsyncMongoClient(**{typo: "standard"}) # type: ignore[arg-type] + + async def test_detected_environment_logging(self, caplog): + normal_hosts = [ + "normal.host.com", + "host.cosmos.azure.com", + "host.docdb.amazonaws.com", + "host.docdb-elastic.amazonaws.com", + ] + srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] + multi_host = ( + "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" + ) + with caplog.at_level(logging.INFO, logger="pymongo"): + with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: + for host in normal_hosts: + AsyncMongoClient(host, connect=False) + for host in srv_hosts: + mock_get_hosts.return_value = [(host, 1)] + AsyncMongoClient(host, connect=False) + AsyncMongoClient(multi_host, connect=False) + logs = [record.getMessage() for record in caplog.records if record.name == "pymongo.client"] + assert len(logs) == 7 + + async def test_detected_environment_warning(self, caplog): + normal_hosts = [ + "host.cosmos.azure.com", + "host.docdb.amazonaws.com", + "host.docdb-elastic.amazonaws.com", + ] + srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] + multi_host = ( + "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" + ) + with caplog.at_level(logging.WARN, logger="pymongo"): + with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: + with pytest.warns(UserWarning): + for host in normal_hosts: + simple_client(host) + for host in srv_hosts: + mock_get_hosts.return_value = [(host, 1)] + simple_client(host) + simple_client(multi_host) + From cca705d3023ecd400ed09e93aca4583bc0e8e05c Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 22 Jan 2025 12:17:36 -0500 Subject: [PATCH 06/29] TestAsyncClientIntegrationTest converted --- test/asynchronous/__init__.py | 45 +- test/asynchronous/conftest.py | 180 ++- test/asynchronous/test_client_pytest.py | 1572 ++++++++++++++++++++++- 3 files changed, 1664 insertions(+), 133 deletions(-) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index e335755990..e93f023305 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -879,6 +879,19 @@ async def max_message_size_bytes(self): # Reusable client context async_client_context = AsyncClientContext() +class AsyncPyMongoTestCasePyTest: + @asynccontextmanager + async def fail_point(self, client, command_args): + cmd_on = SON([("configureFailPoint", "failCommand")]) + cmd_on.update(command_args) + await client.admin.command(cmd_on) + try: + yield + finally: + await client.admin.command( + "configureFailPoint", cmd_on["configureFailPoint"], mode="off" + ) + class AsyncPyMongoTestCase(unittest.IsolatedAsyncioTestCase): def assertEqualCommand(self, expected, actual, msg=None): @@ -1205,39 +1218,7 @@ async def asyncTearDown(self) -> None: await super().asyncTearDown() -async def _get_environment(): - client = AsyncClientContext() - await client.init() - requirements = {} - requirements["SUPPORT_TRANSACTIONS"] = client.supports_transactions() - requirements["IS_DATA_LAKE"] = client.is_data_lake - requirements["IS_SYNC"] = _IS_SYNC - requirements["IS_SYNC"] = _IS_SYNC - requirements["REQUIRE_API_VERSION"] = MONGODB_API_VERSION - requirements["SUPPORTS_FAILCOMMAND_FAIL_POINT"] = client.supports_failCommand_fail_point - requirements["IS_NOT_MMAP"] = client.is_not_mmap - requirements["SERVER_VERSION"] = client.version - requirements["AUTH_ENABLED"] = client.auth_enabled - requirements["FIPS_ENABLED"] = client.fips_enabled - requirements["IS_RS"] = client.is_rs - requirements["MONGOSES"] = len(client.mongoses) - requirements["SECONDARIES_COUNT"] = await client.secondaries_count - requirements["SECONDARY_READ_PREF"] = await client.supports_secondary_read_pref - requirements["HAS_IPV6"] = client.has_ipv6 - requirements["IS_SERVERLESS"] = client.serverless - requirements["IS_LOAD_BALANCER"] = client.load_balancer - requirements["TEST_COMMANDS_ENABLED"] = client.test_commands_enabled - requirements["IS_TLS"] = client.tls - requirements["IS_TLS_CERT"] = client.tlsCertificateKeyFile - requirements["SERVER_IS_RESOLVEABLE"] = client.server_is_resolvable - requirements["SESSIONS_ENABLED"] = client.sessions_enabled - requirements["SUPPORTS_RETRYABLE_WRITES"] = client.supports_retryable_writes() - await client.client.close() - - return requirements - async def async_setup(): - await _get_environment() warnings.resetwarnings() warnings.simplefilter("always") global_knobs.enable() diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index 7b1df58357..22fd839d17 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -1,7 +1,9 @@ from __future__ import annotations import asyncio +import contextlib import sys +from typing import Callable import pymongo from typing_extensions import Any @@ -9,7 +11,7 @@ from pymongo import AsyncMongoClient from pymongo.uri_parser import parse_uri -from test import pytest_conf, db_user, db_pwd +from test import pytest_conf, db_user, db_pwd, MONGODB_API_VERSION from test.asynchronous import async_setup, async_teardown, _connection_string, AsyncClientContext import pytest @@ -37,6 +39,86 @@ async def async_client_context_fixture(): yield client await client.client.close() +@pytest_asyncio.fixture(loop_scope="session", autouse=True) +async def test_environment(async_client_context_fixture): + requirements = {} + requirements["SUPPORT_TRANSACTIONS"] = async_client_context_fixture.supports_transactions() + requirements["IS_DATA_LAKE"] = async_client_context_fixture.is_data_lake + requirements["IS_SYNC"] = _IS_SYNC + requirements["IS_SYNC"] = _IS_SYNC + requirements["REQUIRE_API_VERSION"] = MONGODB_API_VERSION + requirements["SUPPORTS_FAILCOMMAND_FAIL_POINT"] = async_client_context_fixture.supports_failCommand_fail_point + requirements["IS_NOT_MMAP"] = async_client_context_fixture.is_not_mmap + requirements["SERVER_VERSION"] = async_client_context_fixture.version + requirements["AUTH_ENABLED"] = async_client_context_fixture.auth_enabled + requirements["FIPS_ENABLED"] = async_client_context_fixture.fips_enabled + requirements["IS_RS"] = async_client_context_fixture.is_rs + requirements["MONGOSES"] = len(async_client_context_fixture.mongoses) + requirements["SECONDARIES_COUNT"] = await async_client_context_fixture.secondaries_count + requirements["SECONDARY_READ_PREF"] = await async_client_context_fixture.supports_secondary_read_pref + requirements["HAS_IPV6"] = async_client_context_fixture.has_ipv6 + requirements["IS_SERVERLESS"] = async_client_context_fixture.serverless + requirements["IS_LOAD_BALANCER"] = async_client_context_fixture.load_balancer + requirements["TEST_COMMANDS_ENABLED"] = async_client_context_fixture.test_commands_enabled + requirements["IS_TLS"] = async_client_context_fixture.tls + requirements["IS_TLS_CERT"] = async_client_context_fixture.tlsCertificateKeyFile + requirements["SERVER_IS_RESOLVEABLE"] = async_client_context_fixture.server_is_resolvable + requirements["SESSIONS_ENABLED"] = async_client_context_fixture.sessions_enabled + requirements["SUPPORTS_RETRYABLE_WRITES"] = async_client_context_fixture.supports_retryable_writes() + yield requirements + + +@pytest_asyncio.fixture +async def require_auth(test_environment): + if not test_environment["AUTH_ENABLED"]: + pytest.skip("Authentication is not enabled on the server") + +@pytest_asyncio.fixture +async def require_no_fips(test_environment): + if test_environment["FIPS_ENABLED"]: + pytest.skip("Test cannot run on a FIPS-enabled host") + +@pytest_asyncio.fixture +async def require_no_tls(test_environment): + if test_environment["IS_TLS"]: + pytest.skip("Must be able to connect without TLS") + +@pytest_asyncio.fixture +async def require_ipv6(test_environment): + if not test_environment["HAS_IPV6"]: + pytest.skip("No IPv6") + +@pytest_asyncio.fixture +async def require_sync(test_environment): + if not _IS_SYNC: + pytest.skip("This test only works with the synchronous API") + +@pytest_asyncio.fixture +async def require_no_mongos(test_environment): + if test_environment["MONGOSES"]: + pytest.skip("Must be connected to a mongod, not a mongos") + +@pytest_asyncio.fixture +async def require_no_replica_set(test_environment): + if test_environment["IS_RS"]: + pytest.skip("Connected to a replica set, not a standalone mongod") + +@pytest_asyncio.fixture +async def require_replica_set(test_environment): + if not test_environment["IS_RS"]: + pytest.skip("Not connected to a replica set") + +@pytest_asyncio.fixture +async def require_sdam(test_environment): + if test_environment["IS_SERVERLESS"] or test_environment["IS_LOAD_BALANCER"]: + pytest.skip("loadBalanced and serverless clients do not run SDAM") + +@pytest_asyncio.fixture +async def require_failCommand_fail_point(test_environment): + if not test_environment["SUPPORTS_FAILCOMMAND_FAIL_POINT"]: + pytest.skip("failCommand fail point must be supported") + + @pytest_asyncio.fixture(loop_scope="session", autouse=True) async def test_setup_and_teardown(): await async_setup() @@ -80,12 +162,18 @@ async def async_single_client_noauth( ) -> AsyncMongoClient[dict]: """Make a direct connection. Don't authenticate.""" return await _async_mongo_client(async_client_context, h, p, authenticate=False, directConnection=True, **kwargs) -# -async def async_single_client( - async_client_context, h: Any = None, p: Any = None, **kwargs: Any -) -> AsyncMongoClient[dict]: + +@pytest_asyncio.fixture(loop_scope="session") +async def async_single_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: """Make a direct connection, and authenticate if necessary.""" - return await _async_mongo_client(async_client_context, h, p, directConnection=True, **kwargs) + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = await _async_mongo_client(async_client_context_fixture, h, p, directConnection=True, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() # @pytest_asyncio.fixture(loop_scope="function") # async def async_rs_client_noauth( @@ -94,38 +182,64 @@ async def async_single_client( # """Connect to the replica set. Don't authenticate.""" # return await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) # -# @pytest_asyncio.fixture(loop_scope="function") -# async def async_rs_client( -# async_client_context, h: Any = None, p: Any = None, **kwargs: Any -# ) -> AsyncMongoClient[dict]: -# """Connect to the replica set and authenticate if necessary.""" -# return await _async_mongo_client(async_client_context, h, p, **kwargs) -# -# @pytest_asyncio.fixture(loop_scope="function") -# async def async_rs_or_single_client_noauth( -# async_client_context, h: Any = None, p: Any = None, **kwargs: Any -# ) -> AsyncMongoClient[dict]: -# """Connect to the replica set if there is one, otherwise the standalone. -# -# Like rs_or_single_client, but does not authenticate. -# """ -# return await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) -async def async_rs_or_single_client( - async_client_context, h: Any = None, p: Any = None, **kwargs: Any -) -> AsyncMongoClient[Any]: +@pytest_asyncio.fixture(loop_scope="session") +async def async_rs_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: + """Connect to the replica set and authenticate if necessary.""" + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = await _async_mongo_client(async_client_context_fixture, h, p, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() + + +@pytest_asyncio.fixture(loop_scope="session") +async def async_rs_or_single_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: + """Connect to the replica set if there is one, otherwise the standalone. + + Like rs_or_single_client, but does not authenticate. + """ + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = await _async_mongo_client(async_client_context_fixture, h, p, authenticate=False, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() + +@pytest_asyncio.fixture(loop_scope="session") +async def async_rs_or_single_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: """Connect to the replica set if there is one, otherwise the standalone. Authenticates if necessary. """ - return await _async_mongo_client(async_client_context, h, p, **kwargs) - -def simple_client(h: Any = None, p: Any = None, **kwargs: Any) -> AsyncMongoClient: - if not h and not p: - client = AsyncMongoClient(**kwargs) - else: - client = AsyncMongoClient(h, p, **kwargs) - return client + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = await _async_mongo_client(async_client_context_fixture, h, p, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() + + +@pytest_asyncio.fixture(loop_scope="session") +async def simple_client() -> Callable[..., AsyncMongoClient]: + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + if not h and not p: + client = AsyncMongoClient(**kwargs) + else: + client = AsyncMongoClient(h, p, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() @pytest.fixture(scope="function") def patch_resolver(): diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py index adc03f4249..17c39861e9 100644 --- a/test/asynchronous/test_client_pytest.py +++ b/test/asynchronous/test_client_pytest.py @@ -43,7 +43,8 @@ from bson.binary import CSHARP_LEGACY, JAVA_LEGACY, PYTHON_LEGACY, Binary, UuidRepresentation from pymongo.operations import _Op -from test.asynchronous.conftest import async_rs_or_single_client, simple_client, async_single_client +from test.asynchronous.conftest import simple_client, async_single_client, async_rs_client, \ + async_rs_or_single_client sys.path[0:0] = [""] @@ -58,7 +59,7 @@ db_pwd, db_user, remove_all_users, - unittest, AsyncClientContext, + unittest, AsyncClientContext, AsyncPyMongoTestCasePyTest, ) from test.asynchronous.pymongo_mocks import AsyncMockClient from test.test_binary import BinaryData @@ -131,15 +132,15 @@ class TestAsyncClientUnitTest: @pytest_asyncio.fixture(loop_scope="session") - async def async_client(self, async_client_context_fixture) -> AsyncMongoClient: - client = await async_rs_or_single_client(async_client_context_fixture, + async def async_client(self, async_rs_or_single_client) -> AsyncMongoClient: + client = await async_rs_or_single_client( connect=False, serverSelectionTimeoutMS=100 ) yield client await client.close() - async def test_keyword_arg_defaults(self): - client = simple_client( + async def test_keyword_arg_defaults(self, simple_client): + client = await simple_client( socketTimeoutMS=None, connectTimeoutMS=20000, waitQueueTimeoutMS=None, @@ -164,18 +165,18 @@ async def test_keyword_arg_defaults(self): assert client.read_preference == ReadPreference.PRIMARY assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 - async def test_connect_timeout(self): - client = simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) + async def test_connect_timeout(self, simple_client): + client = await simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) pool_opts = client.options.pool_options assert pool_opts.socket_timeout is None assert pool_opts.connect_timeout is None - client = simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) + client = await simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) pool_opts = client.options.pool_options assert pool_opts.socket_timeout is None assert pool_opts.connect_timeout is None - client = simple_client( + client = await simple_client( "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False ) pool_opts = client.options.pool_options @@ -197,8 +198,8 @@ async def test_types(self): with pytest.raises(ConfigurationError): AsyncMongoClient([]) - async def test_max_pool_size_zero(self): - simple_client(maxPoolSize=0) + async def test_max_pool_size_zero(self, simple_client): + await simple_client(maxPoolSize=0) async def test_uri_detection(self): with pytest.raises(ConfigurationError): @@ -278,8 +279,8 @@ async def test_iteration(self, async_client): - async def test_get_default_database(self, async_client_context_fixture): - c = await async_rs_or_single_client(async_client_context_fixture, + async def test_get_default_database(self, async_rs_or_single_client, async_client_context_fixture): + c = await async_rs_or_single_client( "mongodb://%s:%d/foo" % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, @@ -295,75 +296,75 @@ async def test_get_default_database(self, async_client_context_fixture): assert ReadPreference.SECONDARY == db.read_preference assert write_concern == db.write_concern - c = await async_rs_or_single_client(async_client_context_fixture, + c = await async_rs_or_single_client( "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) assert AsyncDatabase(c, "foo") == c.get_default_database("foo") - async def test_get_default_database_error(self, async_client_context_fixture): + async def test_get_default_database_error(self, async_rs_or_single_client, async_client_context_fixture): # URI with no database. - c = await async_rs_or_single_client(async_client_context_fixture, + c = await async_rs_or_single_client( "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) with pytest.raises(ConfigurationError): c.get_default_database() - async def test_get_default_database_with_authsource(self, async_client_context_fixture): + async def test_get_default_database_with_authsource(self, async_client_context_fixture, async_rs_or_single_client): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( await async_client_context_fixture.host, await async_client_context_fixture.port, ) - c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) + c = await async_rs_or_single_client(uri, connect=False) assert (AsyncDatabase(c, "foo") == c.get_default_database()) - async def test_get_database_default(self, async_client_context_fixture): - c = await async_rs_or_single_client(async_client_context_fixture, + async def test_get_database_default(self, async_client_context_fixture, async_rs_or_single_client): + c = await async_rs_or_single_client( "mongodb://%s:%d/foo" % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) assert AsyncDatabase(c, "foo") == c.get_database() - async def test_get_database_default_error(self, async_client_context_fixture): + async def test_get_database_default_error(self, async_client_context_fixture, async_rs_or_single_client): # URI with no database. - c = await async_rs_or_single_client(async_client_context_fixture, + c = await async_rs_or_single_client( "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) with pytest.raises(ConfigurationError): c.get_database() - async def test_get_database_default_with_authsource(self, async_client_context_fixture): + async def test_get_database_default_with_authsource(self, async_client_context_fixture, async_rs_or_single_client): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( await async_client_context_fixture.host, await async_client_context_fixture.port, ) - c = await async_rs_or_single_client(async_client_context_fixture, uri, connect=False) + c = await async_rs_or_single_client(uri, connect=False) assert AsyncDatabase(c, "foo") == c.get_database() - async def test_primary_read_pref_with_tags(self, async_client_context_fixture): + async def test_primary_read_pref_with_tags(self, async_single_client): # No tags allowed with "primary". with pytest.raises(ConfigurationError): - async with await async_single_client(async_client_context_fixture, "mongodb://host/?readpreferencetags=dc:east"): + async with await async_single_client("mongodb://host/?readpreferencetags=dc:east"): pass with pytest.raises(ConfigurationError): - async with await async_single_client(async_client_context_fixture, + async with await async_single_client( "mongodb://host/?readpreference=primary&readpreferencetags=dc:east" ): pass - async def test_read_preference(self, async_client_context_fixture): - c = await async_rs_or_single_client(async_client_context_fixture, + async def test_read_preference(self, async_client_context_fixture, async_rs_or_single_client): + c = await async_rs_or_single_client( "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode ) assert c.read_preference == ReadPreference.NEAREST - async def test_metadata(self): + async def test_metadata(self, simple_client): metadata = copy.deepcopy(_METADATA) if has_c(): metadata["driver"]["name"] = "PyMongo|c|async" @@ -371,18 +372,18 @@ async def test_metadata(self): metadata["driver"]["name"] = "PyMongo|async" metadata["application"] = {"name": "foobar"} - client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") options = client.options assert options.pool_options.metadata == metadata - client = simple_client("foo", 27017, appname="foobar", connect=False) + client = await simple_client("foo", 27017, appname="foobar", connect=False) options = client.options assert options.pool_options.metadata == metadata # No error - simple_client(appname="x" * 128) + await simple_client(appname="x" * 128) with pytest.raises(ValueError): - simple_client(appname="x" * 129) + await simple_client(appname="x" * 129) # Bad "driver" options. with pytest.raises(TypeError): @@ -392,11 +393,11 @@ async def test_metadata(self): with pytest.raises(TypeError): DriverInfo() with pytest.raises(TypeError): - simple_client(driver=1) + await simple_client(driver=1) with pytest.raises(TypeError): - simple_client(driver="abc") + await simple_client(driver="abc") with pytest.raises(TypeError): - simple_client(driver=("Foo", "1", "a")) + await simple_client(driver=("Foo", "1", "a")) # Test appending to driver info. if has_c(): @@ -405,7 +406,7 @@ async def test_metadata(self): metadata["driver"]["name"] = "PyMongo|async|FooDriver" metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) - client = simple_client( + client = await simple_client( "foo", 27017, appname="foobar", @@ -416,7 +417,7 @@ async def test_metadata(self): assert options.pool_options.metadata == metadata metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) - client = simple_client( + client = await simple_client( "foo", 27017, appname="foobar", @@ -427,14 +428,14 @@ async def test_metadata(self): assert options.pool_options.metadata == metadata # Test truncating driver info metadata. - client = simple_client( + client = await simple_client( driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), connect=False, ) options = client.options assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE - client = simple_client( + client = await simple_client( driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), connect=False, ) @@ -443,18 +444,18 @@ async def test_metadata(self): @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) - async def test_container_metadata(self): + async def test_container_metadata(self, simple_client): metadata = copy.deepcopy(_METADATA) metadata["driver"]["name"] = "PyMongo|async" metadata["env"] = {} metadata["env"]["container"] = {"orchestrator": "kubernetes"} - client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") options = client.options assert options.pool_options.metadata["env"] == metadata["env"] - async def test_kwargs_codec_options(self): + async def test_kwargs_codec_options(self, simple_client): class MyFloatType: def __init__(self, x): self.__x = x @@ -476,7 +477,7 @@ def transform_python(self, value): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" tzinfo = utc - c = simple_client( + c = await simple_client( document_class=document_class, type_registry=type_registry, tz_aware=tz_aware, @@ -493,7 +494,7 @@ def transform_python(self, value): assert c.codec_options.tzinfo == tzinfo - async def test_uri_codec_options(self, async_client_context_fixture): + async def test_uri_codec_options(self, async_client_context_fixture, simple_client): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" datetime_conversion = "DATETIME_CLAMP" @@ -509,20 +510,20 @@ async def test_uri_codec_options(self, async_client_context_fixture): datetime_conversion, ) ) - c = simple_client(uri, connect=False) + c = await simple_client(uri, connect=False) assert c.codec_options.tz_aware is True assert c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] # Change the passed datetime_conversion to a number and re-assert. uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") - c = simple_client(uri, connect=False) + c = await simple_client(uri, connect=False) assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] - async def test_uri_option_precedence(self): + async def test_uri_option_precedence(self, simple_client): # Ensure kwarg options override connection string options. uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" - c = simple_client( + c = await simple_client( uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" ) clopts = c.options @@ -531,7 +532,7 @@ async def test_uri_option_precedence(self): assert clopts.replica_set_name == "newname" assert clopts.read_preference == ReadPreference.SECONDARY_PREFERRED - async def test_connection_timeout_ms_propagates_to_DNS_resolver(self, patch_resolver): + async def test_connection_timeout_ms_propagates_to_DNS_resolver(self, patch_resolver, simple_client): base_uri = "mongodb+srv://test5.test.build.10gen.cc" connectTimeoutMS = 5000 expected_kw_value = 5.0 @@ -539,7 +540,7 @@ async def test_connection_timeout_ms_propagates_to_DNS_resolver(self, patch_reso expected_uri_value = 6.0 async def test_scenario(args, kwargs, expected_value): patch_resolver.reset() - simple_client(*args, **kwargs) + await simple_client(*args, **kwargs) for _, kw in patch_resolver.call_list(): assert pytest.approx(kw["lifetime"], rel=1e-6) == expected_value @@ -557,18 +558,18 @@ async def test_scenario(args, kwargs, expected_value): await test_scenario((uri_with_timeout,), kwarg, expected_kw_value) - async def test_uri_security_options(self): + async def test_uri_security_options(self, simple_client): # Ensure that we don't silently override security-related options. with pytest.raises(InvalidURI): - simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) + await simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) # Matching SSL and TLS options should not cause errors. - c = simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) + c = await simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) assert c.options._options["tls"] is False # Conflicting tlsInsecure options should raise an error. with pytest.raises(InvalidURI): - simple_client( + await simple_client( "mongodb://localhost/?tlsInsecure=true", connect=False, tlsAllowInvalidHostnames=True, @@ -576,7 +577,7 @@ async def test_uri_security_options(self): # Conflicting legacy tlsInsecure options should also raise an error. with pytest.raises(InvalidURI): - simple_client( + await simple_client( "mongodb://localhost/?tlsInsecure=true", connect=False, tlsAllowInvalidCertificates=False, @@ -584,10 +585,10 @@ async def test_uri_security_options(self): # Conflicting kwargs should raise InvalidURI with pytest.raises(InvalidURI): - simple_client(ssl=True, tls=False) + await simple_client(ssl=True, tls=False) - async def test_event_listeners(self): - c = simple_client(event_listeners=[], connect=False) + async def test_event_listeners(self, simple_client): + c = await simple_client(event_listeners=[], connect=False) assert c.options.event_listeners == [] listeners = [ event_loggers.CommandLogger(), @@ -596,11 +597,11 @@ async def test_event_listeners(self): event_loggers.TopologyLogger(), event_loggers.ConnectionPoolLogger(), ] - c = simple_client(event_listeners=listeners, connect=False) + c = await simple_client(event_listeners=listeners, connect=False) assert c.options.event_listeners == listeners - async def test_client_options(self): - c = simple_client(connect=False) + async def test_client_options(self, simple_client): + c = await simple_client(connect=False) assert isinstance(c.options, ClientOptions) assert isinstance(c.options.pool_options, PoolOptions) assert c.options.server_selection_timeout == 30 @@ -641,7 +642,7 @@ async def test_detected_environment_logging(self, caplog): logs = [record.getMessage() for record in caplog.records if record.name == "pymongo.client"] assert len(logs) == 7 - async def test_detected_environment_warning(self, caplog): + async def test_detected_environment_warning(self, caplog, simple_client): normal_hosts = [ "host.cosmos.azure.com", "host.docdb.amazonaws.com", @@ -655,9 +656,1444 @@ async def test_detected_environment_warning(self, caplog): with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: with pytest.warns(UserWarning): for host in normal_hosts: - simple_client(host) + await simple_client(host) for host in srv_hosts: mock_get_hosts.return_value = [(host, 1)] - simple_client(host) - simple_client(multi_host) + await simple_client(host) + await simple_client(multi_host) + +class TestAsyncClientIntegrationTest(AsyncPyMongoTestCasePyTest): + @pytest_asyncio.fixture(loop_scope="session") + async def async_client(self, async_rs_or_single_client) -> AsyncMongoClient: + client = await async_rs_or_single_client( + connect=False, serverSelectionTimeoutMS=100 + ) + yield client + await client.close() + + async def test_multiple_uris(self): + with pytest.raises(ConfigurationError): + AsyncMongoClient( + host=[ + "mongodb+srv://cluster-a.abc12.mongodb.net", + "mongodb+srv://cluster-b.abc12.mongodb.net", + "mongodb+srv://cluster-c.abc12.mongodb.net", + ] + ) + + async def test_max_idle_time_reaper_default(self, async_rs_or_single_client): + with client_knobs(kill_cursor_frequency=0.1): + # Assert reaper doesn't remove connections when maxIdleTimeMS not set + client = await async_rs_or_single_client() + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + async with server._pool.checkout() as conn: + pass + assert 1 == len(server._pool.conns) + assert conn in server._pool.conns + + async def test_max_idle_time_reaper_removes_stale_minPoolSize(self, async_rs_or_single_client): + with client_knobs(kill_cursor_frequency=0.1): + # Assert reaper removes idle socket and replaces it with a new one + client = await async_rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1) + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + async with server._pool.checkout() as conn: + pass + # When the reaper runs at the same time as the get_socket, two + # connections could be created and checked into the pool. + assert len(server._pool.conns) >= 1 + await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") + await async_wait_until(lambda: len(server._pool.conns) >= 1, "replace stale socket") + + async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self, async_rs_or_single_client): + with client_knobs(kill_cursor_frequency=0.1): + # Assert reaper respects maxPoolSize when adding new connections. + client = await async_rs_or_single_client( + maxIdleTimeMS=500, minPoolSize=1, maxPoolSize=1 + ) + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + async with server._pool.checkout() as conn: + pass + # When the reaper runs at the same time as the get_socket, + # maxPoolSize=1 should prevent two connections from being created. + assert 1 == len(server._pool.conns) + await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") + await async_wait_until(lambda: len(server._pool.conns) == 1, "replace stale socket") + + async def test_max_idle_time_reaper_removes_stale(self, async_rs_or_single_client): + with client_knobs(kill_cursor_frequency=0.1): + # Assert that the reaper has removed the idle socket and NOT replaced it. + client = await async_rs_or_single_client(maxIdleTimeMS=500) + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + async with server._pool.checkout() as conn_one: + pass + # Assert that the pool does not close connections prematurely + await asyncio.sleep(0.300) + async with server._pool.checkout() as conn_two: + pass + assert conn_one is conn_two + await async_wait_until( + lambda: len(server._pool.conns) == 0, + "stale socket reaped and new one NOT added to the pool", + ) + + async def test_min_pool_size(self, async_rs_or_single_client): + with client_knobs(kill_cursor_frequency=0.1): + client = await async_rs_or_single_client() + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + assert len(server._pool.conns) == 0 + + # Assert that pool started up at minPoolSize + client = await async_rs_or_single_client(minPoolSize=10) + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + await async_wait_until( + lambda: len(server._pool.conns) == 10, + "pool initialized with 10 connections", + ) + # Assert that if a socket is closed, a new one takes its place. + async with server._pool.checkout() as conn: + conn.close_conn(None) + await async_wait_until( + lambda: len(server._pool.conns) == 10, + "a closed socket gets replaced from the pool", + ) + assert conn not in server._pool.conns + + async def test_max_idle_time_checkout(self, async_rs_or_single_client): + # Use high frequency to test _get_socket_no_auth. + with client_knobs(kill_cursor_frequency=99999999): + client = await async_rs_or_single_client(maxIdleTimeMS=500) + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + async with server._pool.checkout() as conn: + pass + assert len(server._pool.conns) == 1 + await asyncio.sleep(1) # Sleep so that the socket becomes stale. + + async with server._pool.checkout() as new_conn: + assert conn != new_conn + assert len(server._pool.conns) == 1 + assert conn not in server._pool.conns + assert new_conn in server._pool.conns + + # Test that connections are reused if maxIdleTimeMS is not set. + client = await async_rs_or_single_client() + server = await (await client._get_topology()).select_server( + readable_server_selector, _Op.TEST + ) + async with server._pool.checkout() as conn: + pass + assert len(server._pool.conns) == 1 + await asyncio.sleep(1) + async with server._pool.checkout() as new_conn: + assert conn == new_conn + assert len(server._pool.conns) == 1 + + async def test_constants(self, async_client_context_fixture, simple_client): + """This test uses AsyncMongoClient explicitly to make sure that host and + port are not overloaded. + """ + host, port = await async_client_context_fixture.host, await async_client_context_fixture.port + kwargs: dict = async_client_context_fixture.default_client_options.copy() + if async_client_context_fixture.auth_enabled: + kwargs["username"] = "user" # TODO: Replace with correctly managed auth creds + kwargs["password"] = "password" + + # Set bad defaults. + AsyncMongoClient.HOST = "somedomainthatdoesntexist.org" + AsyncMongoClient.PORT = 123456789 + with pytest.raises(AutoReconnect): + c = await simple_client(serverSelectionTimeoutMS=10, **kwargs) + await connected(c) + c = await simple_client(host, port, **kwargs) + # Override the defaults. No error. + await connected(c) + # Set good defaults. + AsyncMongoClient.HOST = host + AsyncMongoClient.PORT = port + # No error. + c = await simple_client(**kwargs) + await connected(c) + + async def test_init_disconnected(self, async_client_context_fixture, async_rs_or_single_client, simple_client): + host, port = await async_client_context_fixture.host, await async_client_context_fixture.port + c = await async_rs_or_single_client(connect=False) + # is_primary causes client to block until connected + assert isinstance(await c.is_primary, bool) + c = await async_rs_or_single_client(connect=False) + assert isinstance(await c.is_mongos, bool) + c = await async_rs_or_single_client(connect=False) + assert isinstance(c.options.pool_options.max_pool_size, int) + assert isinstance(c.nodes, frozenset) + + c = await async_rs_or_single_client(connect=False) + assert c.codec_options == CodecOptions() + c = await async_rs_or_single_client(connect=False) + assert not await c.primary + assert not await c.secondaries + c = await async_rs_or_single_client(connect=False) + assert isinstance(c.topology_description, TopologyDescription) + assert c.topology_description == c._topology._description + if async_client_context_fixture.is_rs: + # The primary's host and port are from the replica set config. + assert await c.address is not None + else: + assert await c.address == (host, port) + bad_host = "somedomainthatdoesntexist.org" + c = await simple_client(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10) + with pytest.raises(ConnectionFailure): + await c.pymongo_test.test.find_one() + + async def test_init_disconnected_with_auth(self, simple_client): + uri = "mongodb://user:pass@somedomainthatdoesntexist" + c = await simple_client(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10) + with pytest.raises(ConnectionFailure): + await c.pymongo_test.test.find_one() + + async def test_equality(self, async_client_context_fixture, async_client, async_rs_or_single_client, simple_client): + seed = "{}:{}".format(*list(async_client._topology_settings.seeds)[0]) + c = await async_rs_or_single_client(seed, connect=False) + assert async_client_context_fixture.client == c + # Explicitly test inequality + assert not async_client_context_fixture.client != c + + c = await async_rs_or_single_client("invalid.com", connect=False) + assert async_client_context_fixture.client != c + assert async_client_context_fixture.client != c + + c1 = await simple_client("a", connect=False) + c2 = await simple_client("b", connect=False) + + # Seeds differ: + assert c1 != c2 + + c1 = await simple_client(["a", "b", "c"], connect=False) + c2 = await simple_client(["c", "a", "b"], connect=False) + + # Same seeds but out of order still compares equal: + assert c1 == c2 + + async def test_hashable(self, async_client_context_fixture, async_client, async_rs_or_single_client): + seed = "{}:{}".format(*list(async_client._topology_settings.seeds)[0]) + c = await async_rs_or_single_client(seed, connect=False) + assert c in {async_client_context_fixture.client} + c = await async_rs_or_single_client("invalid.com", connect=False) + assert c not in {async_client_context_fixture.client} + + async def test_host_w_port(self, async_client_context_fixture): + with pytest.raises(ValueError): + host = await async_client_context_fixture.host + await connected( + AsyncMongoClient( + f"{host}:1234567", + connectTimeoutMS=1, + serverSelectionTimeoutMS=10, + ) + ) + + async def test_repr(self, simple_client): + # Used to test 'eval' below. + import bson + + client = AsyncMongoClient( # type: ignore[type-var] + "mongodb://localhost:27017,localhost:27018/?replicaSet=replset" + "&connectTimeoutMS=12345&w=1&wtimeoutms=100", + connect=False, + document_class=SON, + ) + the_repr = repr(client) + assert "AsyncMongoClient(host=" in the_repr + assert "document_class=bson.son.SON, tz_aware=False, connect=False, " in the_repr + assert "connecttimeoutms=12345" in the_repr + assert "replicaset='replset'" in the_repr + assert "w=1" in the_repr + assert "wtimeoutms=100" in the_repr + async with eval(the_repr) as client_two: + assert client_two == client + client = await simple_client( + "localhost:27017,localhost:27018", + replicaSet="replset", + connectTimeoutMS=12345, + socketTimeoutMS=None, + w=1, + wtimeoutms=100, + connect=False, + ) + the_repr = repr(client) + assert "AsyncMongoClient(host=" in the_repr + assert "document_class=dict, tz_aware=False, connect=False, " in the_repr + assert "connecttimeoutms=12345" in the_repr + assert "replicaset='replset'" in the_repr + assert "sockettimeoutms=None" in the_repr + assert "w=1" in the_repr + assert "wtimeoutms=100" in the_repr + async with eval(the_repr) as client_two: + assert client_two == client + + # async def test_getters(self, async_client, async_client_context_fixture): + # await async_wait_until( + # lambda: async_client_context_fixture.nodes == async_client.nodes, "find all nodes" + # ) + + async def test_list_databases(self, async_client, async_rs_or_single_client): + cmd_docs = (await async_client.admin.command("listDatabases"))["databases"] + cursor = await async_client.list_databases() + assert isinstance(cursor, AsyncCommandCursor) + helper_docs = await cursor.to_list() + assert len(helper_docs) > 0 + assert len(helper_docs) == len(cmd_docs) + # PYTHON-3529 Some fields may change between calls, just compare names. + for helper_doc, cmd_doc in zip(helper_docs, cmd_docs): + assert isinstance(helper_doc, dict) + assert helper_doc.keys() == cmd_doc.keys() + + client_doc = await async_rs_or_single_client(document_class=SON) + async for doc in await client_doc.list_databases(): + assert isinstance(doc, dict) + + await async_client.pymongo_test.test.insert_one({}) + cursor = await async_client.list_databases(filter={"name": "admin"}) + docs = await cursor.to_list() + assert len(docs) == 1 + assert docs[0]["name"] == "admin" + + cursor = await async_client.list_databases(nameOnly=True) + async for doc in cursor: + assert list(doc) == ["name"] + + async def test_list_database_names(self, async_client): + await async_client.pymongo_test.test.insert_one({"dummy": "object"}) + await async_client.pymongo_test_mike.test.insert_one({"dummy": "object"}) + cmd_docs = (await async_client.admin.command("listDatabases"))["databases"] + cmd_names = [doc["name"] for doc in cmd_docs] + + db_names = await async_client.list_database_names() + assert "pymongo_test" in db_names + assert "pymongo_test_mike" in db_names + assert db_names == cmd_names + + async def test_drop_database(self, async_client_context_fixture, async_client, async_rs_or_single_client): + with pytest.raises(TypeError): + await async_client.drop_database(5) # type: ignore[arg-type] + with pytest.raises(TypeError): + await async_client.drop_database(None) # type: ignore[arg-type] + + await async_client.pymongo_test.test.insert_one({"dummy": "object"}) + await async_client.pymongo_test2.test.insert_one({"dummy": "object"}) + dbs = await async_client.list_database_names() + assert "pymongo_test" in dbs + assert "pymongo_test2" in dbs + await async_client.drop_database("pymongo_test") + + if async_client_context_fixture.is_rs: + wc_client = await async_rs_or_single_client(w=len(async_client_context_fixture.nodes) + 1) + with pytest.raises(WriteConcernError): + await wc_client.drop_database("pymongo_test2") + + await async_client.drop_database(async_client.pymongo_test2) + dbs = await async_client.list_database_names() + assert "pymongo_test" not in dbs + assert "pymongo_test2" not in dbs + + async def test_close(self, async_rs_or_single_client): + test_client = await async_rs_or_single_client() + coll = test_client.pymongo_test.bar + await test_client.close() + with pytest.raises(InvalidOperation): + await coll.count_documents({}) + + async def test_close_kills_cursors(self, async_rs_or_single_client): + if sys.platform.startswith("java"): + # We can't figure out how to make this test reliable with Jython. + raise SkipTest("Can't test with Jython") + test_client = await async_rs_or_single_client() + # Kill any cursors possibly queued up by previous tests. + gc.collect() + await test_client._process_periodic_tasks() + + # Add some test data. + coll = test_client.pymongo_test.test_close_kills_cursors + docs_inserted = 1000 + await coll.insert_many([{"i": i} for i in range(docs_inserted)]) + + # Open a cursor and leave it open on the server. + cursor = coll.find().batch_size(10) + assert bool(await anext(cursor)) + assert cursor.retrieved < docs_inserted + + # Open a command cursor and leave it open on the server. + cursor = await coll.aggregate([], batchSize=10) + assert bool(await anext(cursor)) + del cursor + # Required for PyPy, Jython and other Python implementations that + # don't use reference counting garbage collection. + gc.collect() + + # Close the client and ensure the topology is closed. + assert test_client._topology._opened + await test_client.close() + assert not test_client._topology._opened + test_client = await async_rs_or_single_client() + # The killCursors task should not need to re-open the topology. + await test_client._process_periodic_tasks() + assert test_client._topology._opened + + async def test_close_stops_kill_cursors_thread(self, async_rs_client): + client = await async_rs_client() + await client.test.test.find_one() + assert not client._kill_cursors_executor._stopped + + # Closing the client should stop the thread. + await client.close() + assert client._kill_cursors_executor._stopped + + # Reusing the closed client should raise an InvalidOperation error. + with pytest.raises(InvalidOperation): + await client.admin.command("ping") + # Thread is still stopped. + assert client._kill_cursors_executor._stopped + + async def test_uri_connect_option(self, async_rs_client): + # Ensure that topology is not opened if connect=False. + client = await async_rs_client(connect=False) + assert not client._topology._opened + + # Ensure kill cursors thread has not been started. + if _IS_SYNC: + kc_thread = client._kill_cursors_executor._thread + assert not (kc_thread and kc_thread.is_alive()) + else: + kc_task = client._kill_cursors_executor._task + assert not (kc_task and not kc_task.done()) + # Using the client should open topology and start the thread. + await client.admin.command("ping") + assert client._topology._opened + if _IS_SYNC: + kc_thread = client._kill_cursors_executor._thread + assert kc_thread and kc_thread.is_alive() + else: + kc_task = client._kill_cursors_executor._task + assert kc_task and not kc_task.done() + + async def test_close_does_not_open_servers(self, async_rs_client): + client = await async_rs_client(connect=False) + topology = client._topology + assert topology._servers == {} + await client.close() + assert topology._servers == {} + + async def test_close_closes_sockets(self, async_rs_client): + client = await async_rs_client() + await client.test.test.find_one() + topology = client._topology + await client.close() + for server in topology._servers.values(): + assert not server._pool.conns + assert server._monitor._executor._stopped + assert server._monitor._rtt_monitor._executor._stopped + assert not server._monitor._pool.conns + assert not server._monitor._rtt_monitor._pool.conns + + async def test_bad_uri(self): + with pytest.raises(InvalidURI): + AsyncMongoClient("http://localhost") + + @pytest.mark.usefixtures("require_auth") + @pytest.mark.usefixtures("require_no_fips") + async def test_auth_from_uri(self, async_client_context_fixture, async_rs_or_single_client_noauth): + host, port = await async_client_context_fixture.host, await async_client_context_fixture.port + await async_client_context_fixture.create_user("admin", "admin", "pass") + # TODO + # self.addAsyncCleanup(async_client_context.drop_user, "admin", "admin") + # self.addAsyncCleanup(remove_all_users, self.client.pymongo_test) + + await async_client_context_fixture.create_user( + "pymongo_test", "user", "pass", roles=["userAdmin", "readWrite"] + ) + + with pytest.raises(OperationFailure): + await connected( + await async_rs_or_single_client_noauth("mongodb://a:b@%s:%d" % (host, port)) + ) + + # No error. + await connected( + await async_rs_or_single_client_noauth("mongodb://admin:pass@%s:%d" % (host, port)) + ) + + # Wrong database. + uri = "mongodb://admin:pass@%s:%d/pymongo_test" % (host, port) + with pytest.raises(OperationFailure): + await connected(await async_rs_or_single_client_noauth(uri)) + + # No error. + await connected( + await async_rs_or_single_client_noauth( + "mongodb://user:pass@%s:%d/pymongo_test" % (host, port) + ) + ) + + # Auth with lazy connection. + await ( + await async_rs_or_single_client_noauth( + "mongodb://user:pass@%s:%d/pymongo_test" % (host, port), connect=False + ) + ).pymongo_test.test.find_one() + + # Wrong password. + bad_client = await async_rs_or_single_client_noauth( + "mongodb://user:wrong@%s:%d/pymongo_test" % (host, port), connect=False + ) + + with pytest.raises(OperationFailure): + await bad_client.pymongo_test.test.find_one() + + @pytest.mark.usefixtures("require_auth") + async def test_username_and_password(self, async_client_context_fixture, async_rs_or_single_client_noauth): + await async_client_context_fixture.create_user("admin", "ad min", "pa/ss") + # TODO + # self.addAsyncCleanup(async_client_context.drop_user, "admin", "ad min") + + c = await async_rs_or_single_client_noauth(username="ad min", password="pa/ss") + + # Username and password aren't in strings that will likely be logged. + assert "ad min" not in repr(c) + assert "ad min" not in str(c) + assert "pa/ss" not in repr(c) + assert "pa/ss" not in str(c) + + # Auth succeeds. + await c.server_info() + + with pytest.raises(OperationFailure): + await ( + await async_rs_or_single_client_noauth(username="ad min", password="foo") + ).server_info() + + @pytest.mark.usefixtures("require_auth") + @pytest.mark.usefixtures("require_no_fips") + async def test_lazy_auth_raises_operation_failure(self, async_client_context_fixture, async_rs_or_single_client_noauth): + host = await async_client_context_fixture.host + lazy_client = await async_rs_or_single_client_noauth( + f"mongodb://user:wrong@{host}/pymongo_test", connect=False + ) + + await asyncAssertRaisesExactly(OperationFailure, lazy_client.test.collection.find_one) + + + @pytest.mark.usefixtures("require_no_tls") + async def test_unix_socket(self, async_client_context_fixture, async_rs_or_single_client, simple_client): + if not hasattr(socket, "AF_UNIX"): + pytest.skip("UNIX-sockets are not supported on this system") + + mongodb_socket = "/tmp/mongodb-%d.sock" % (await async_client_context_fixture.port,) + encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (await async_client_context_fixture.port,) + if not os.access(mongodb_socket, os.R_OK): + pytest.skip("Socket file is not accessible") + + uri = "mongodb://%s" % encoded_socket + # Confirm we can do operations via the socket. + client = await async_rs_or_single_client(uri) + await client.pymongo_test.test.insert_one({"dummy": "object"}) + dbs = await client.list_database_names() + assert "pymongo_test" in dbs + + assert mongodb_socket in repr(client) + + # Confirm it fails with a missing socket. + with pytest.raises(ConnectionFailure): + c = await simple_client( + "mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100 + ) + await connected(c) + + async def test_document_class(self, async_client, async_rs_or_single_client): + c = async_client + db = c.pymongo_test + await db.test.insert_one({"x": 1}) + + assert dict == c.codec_options.document_class + assert isinstance(await db.test.find_one(), dict) + assert not isinstance(await db.test.find_one(), SON) + + c = await async_rs_or_single_client(document_class=SON) + + db = c.pymongo_test + + assert SON == c.codec_options.document_class + assert isinstance(await db.test.find_one(), SON) + + + async def test_timeouts(self, async_rs_or_single_client): + client = await async_rs_or_single_client( + connectTimeoutMS=10500, + socketTimeoutMS=10500, + maxIdleTimeMS=10500, + serverSelectionTimeoutMS=10500, + ) + assert 10.5 == (await async_get_pool(client)).opts.connect_timeout + assert 10.5 == (await async_get_pool(client)).opts.socket_timeout + assert 10.5 == (await async_get_pool(client)).opts.max_idle_time_seconds + assert 10.5 == client.options.pool_options.max_idle_time_seconds + assert 10.5 == client.options.server_selection_timeout + + async def test_socket_timeout_ms_validation(self, async_rs_or_single_client): + c = await async_rs_or_single_client(socketTimeoutMS=10 * 1000) + assert 10 == (await async_get_pool(c)).opts.socket_timeout + + c = await connected(await async_rs_or_single_client(socketTimeoutMS=None)) + assert None == (await async_get_pool(c)).opts.socket_timeout + + c = await connected(await async_rs_or_single_client(socketTimeoutMS=0)) + assert None == (await async_get_pool(c)).opts.socket_timeout + + with pytest.raises(ValueError): + async with await async_rs_or_single_client(socketTimeoutMS=-1): + pass + + with pytest.raises(ValueError): + async with await async_rs_or_single_client(socketTimeoutMS=1e10): + pass + + with pytest.raises(ValueError): + async with await async_rs_or_single_client(socketTimeoutMS="foo"): + pass + + async def test_socket_timeout(self, async_client, async_rs_or_single_client): + no_timeout = async_client + timeout_sec = 1 + timeout = await async_rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) + + await no_timeout.pymongo_test.drop_collection("test") + await no_timeout.pymongo_test.test.insert_one({"x": 1}) + + # A $where clause that takes a second longer than the timeout + where_func = delay(timeout_sec + 1) + + async def get_x(db): + doc = await anext(db.test.find().where(where_func)) + return doc["x"] + + assert 1 == await get_x(no_timeout.pymongo_test) + with pytest.raises(NetworkTimeout): + await get_x(timeout.pymongo_test) + + async def test_server_selection_timeout(self): + client = AsyncMongoClient(serverSelectionTimeoutMS=100, connect=False) + pytest.approx(client.options.server_selection_timeout, 0.1) + await client.close() + + client = AsyncMongoClient(serverSelectionTimeoutMS=0, connect=False) + + pytest.approx(client.options.server_selection_timeout, 0) + + pytest.raises( + ValueError, AsyncMongoClient, serverSelectionTimeoutMS="foo", connect=False + ) + pytest.raises(ValueError, AsyncMongoClient, serverSelectionTimeoutMS=-1, connect=False) + pytest.raises( + ConfigurationError, AsyncMongoClient, serverSelectionTimeoutMS=None, connect=False + ) + await client.close() + + client = AsyncMongoClient( + "mongodb://localhost/?serverSelectionTimeoutMS=100", connect=False + ) + pytest.approx(client.options.server_selection_timeout, 0.1) + await client.close() + + client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=0", connect=False) + pytest.approx(client.options.server_selection_timeout, 0) + await client.close() + + # Test invalid timeout in URI ignored and set to default. + client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=-1", connect=False) + pytest.approx(client.options.server_selection_timeout, 30) + await client.close() + + client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=", connect=False) + pytest.approx(client.options.server_selection_timeout, 30) + + async def test_waitQueueTimeoutMS(self, async_rs_or_single_client): + client = await async_rs_or_single_client(waitQueueTimeoutMS=2000) + assert 2 == (await async_get_pool(client)).opts.wait_queue_timeout + + async def test_socketKeepAlive(self, async_client): + pool = await async_get_pool(async_client) + async with pool.checkout() as conn: + keepalive = conn.conn.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) + assert keepalive + + @no_type_check + async def test_tz_aware(self, async_client, async_rs_or_single_client): + pytest.raises(ValueError, AsyncMongoClient, tz_aware="foo") + + aware = await async_rs_or_single_client(tz_aware=True) + naive = async_client + await aware.pymongo_test.drop_collection("test") + + now = datetime.datetime.now(tz=datetime.timezone.utc) + await aware.pymongo_test.test.insert_one({"x": now}) + + assert None == (await naive.pymongo_test.test.find_one())["x"].tzinfo + assert utc == (await aware.pymongo_test.test.find_one())["x"].tzinfo + assert ( + (await aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None) == + (await naive.pymongo_test.test.find_one())["x"] + ) + + @pytest.mark.usefixtures("require_ipv6") + async def test_ipv6(self, async_client_context_fixture, async_rs_or_single_client_noauth): + if async_client_context_fixture.tls: + if not HAVE_IPADDRESS: + pytest.skip("Need the ipaddress module to test with SSL") + + if async_client_context_fixture.auth_enabled: + auth_str = f"{db_user}:{db_pwd}@" + else: + auth_str = "" + + uri = "mongodb://%s[::1]:%d" % (auth_str, await async_client_context_fixture.port) + if async_client_context_fixture.is_rs: + uri += "/?replicaSet=" + (async_client_context_fixture.replica_set_name or "") + + client = await async_rs_or_single_client_noauth(uri) + await client.pymongo_test.test.insert_one({"dummy": "object"}) + await client.pymongo_test_bernie.test.insert_one({"dummy": "object"}) + + dbs = await client.list_database_names() + assert "pymongo_test" in dbs + assert "pymongo_test_bernie" in dbs + + async def test_contextlib(self, async_rs_or_single_client): + client = await async_rs_or_single_client() + await client.pymongo_test.drop_collection("test") + await client.pymongo_test.test.insert_one({"foo": "bar"}) + + # The socket used for the previous commands has been returned to the + # pool + assert 1 == len((await async_get_pool(client)).conns) + + # contextlib async support was added in Python 3.10 + if _IS_SYNC or sys.version_info >= (3, 10): + async with contextlib.aclosing(client): + assert "bar" == (await client.pymongo_test.test.find_one())["foo"] + with pytest.raises(InvalidOperation): + await client.pymongo_test.test.find_one() + client = await async_rs_or_single_client() + async with client as client: + assert "bar" == (await client.pymongo_test.test.find_one())["foo"] + with pytest.raises(InvalidOperation): + await client.pymongo_test.test.find_one() + + @pytest.mark.usefixtures("require_sync") + def test_interrupt_signal(self, async_client): + if sys.platform.startswith("java"): + # We can't figure out how to raise an exception on a thread that's + # blocked on a socket, whether that's the main thread or a worker, + # without simply killing the whole thread in Jython. This suggests + # PYTHON-294 can't actually occur in Jython. + pytest.skip("Can't test interrupts in Jython") + if is_greenthread_patched(): + pytest.skip("Can't reliably test interrupts with green threads") + + # Test fix for PYTHON-294 -- make sure AsyncMongoClient closes its + # socket if it gets an interrupt while waiting to recv() from it. + db = async_client.pymongo_test + + # A $where clause which takes 1.5 sec to execute + where = delay(1.5) + + # Need exactly 1 document so find() will execute its $where clause once + db.drop_collection("foo") + db.foo.insert_one({"_id": 1}) + + old_signal_handler = None + try: + # Platform-specific hacks for raising a KeyboardInterrupt on the + # main thread while find() is in-progress: On Windows, SIGALRM is + # unavailable so we use a second thread. In our Evergreen setup on + # Linux, the thread technique causes an error in the test at + # conn.recv(): TypeError: 'int' object is not callable + # We don't know what causes this, so we hack around it. + + if sys.platform == "win32": + + def interrupter(): + # Raises KeyboardInterrupt in the main thread + time.sleep(0.25) + thread.interrupt_main() + + thread.start_new_thread(interrupter, ()) + else: + # Convert SIGALRM to SIGINT -- it's hard to schedule a SIGINT + # for one second in the future, but easy to schedule SIGALRM. + def sigalarm(num, frame): + raise KeyboardInterrupt + + old_signal_handler = signal.signal(signal.SIGALRM, sigalarm) + signal.alarm(1) + + raised = False + try: + # Will be interrupted by a KeyboardInterrupt. + next(db.foo.find({"$where": where})) # type: ignore[call-overload] + except KeyboardInterrupt: + raised = True + + assert raised, "Didn't raise expected KeyboardInterrupt" + + # Raises AssertionError due to PYTHON-294 -- Mongo's response to + # the previous find() is still waiting to be read on the socket, + # so the request id's don't match. + assert {"_id": 1} == next(db.foo.find()) # type: ignore[call-overload] + finally: + if old_signal_handler: + signal.signal(signal.SIGALRM, old_signal_handler) + + async def test_operation_failure(self, async_single_client): + # Ensure AsyncMongoClient doesn't close socket after it gets an error + # response to getLastError. PYTHON-395. We need a new client here + # to avoid race conditions caused by replica set failover or idle + # socket reaping. + client = await async_single_client() + await client.pymongo_test.test.find_one() + pool = await async_get_pool(client) + socket_count = len(pool.conns) + assert socket_count >= 1 + old_conn = next(iter(pool.conns)) + await client.pymongo_test.test.drop() + await client.pymongo_test.test.insert_one({"_id": "foo"}) + with pytest.raises(OperationFailure): + await client.pymongo_test.test.insert_one({"_id": "foo"}) + + assert socket_count == len(pool.conns) + new_conn = next(iter(pool.conns)) + assert old_conn == new_conn + + async def test_lazy_connect_w0(self, async_client_context_fixture, async_rs_or_single_client): + # Ensure that connect-on-demand works when the first operation is + # an unacknowledged write. This exercises _writable_max_wire_version(). + + # Use a separate collection to avoid races where we're still + # completing an operation on a collection while the next test begins. + await async_client_context_fixture.client.drop_database("test_lazy_connect_w0") + # TODO + # self.addAsyncCleanup(async_client_context.client.drop_database, "test_lazy_connect_w0") + + client = await async_rs_or_single_client(connect=False, w=0) + await client.test_lazy_connect_w0.test.insert_one({}) + + async def predicate(): + return await client.test_lazy_connect_w0.test.count_documents({}) == 1 + + await async_wait_until(predicate, "find one document") + + client = await async_rs_or_single_client(connect=False, w=0) + await client.test_lazy_connect_w0.test.update_one({}, {"$set": {"x": 1}}) + + async def predicate(): + return (await client.test_lazy_connect_w0.test.find_one()).get("x") == 1 + + await async_wait_until(predicate, "update one document") + + client = await async_rs_or_single_client(connect=False, w=0) + await client.test_lazy_connect_w0.test.delete_one({}) + + async def predicate(): + return await client.test_lazy_connect_w0.test.count_documents({}) == 0 + + await async_wait_until(predicate, "delete one document") + + + @pytest.mark.usefixtures("require_no_mongos") + async def test_exhaust_network_error(self, async_rs_or_single_client): + # When doing an exhaust query, the socket stays checked out on success + # but must be checked in on error to avoid semaphore leaks. + client = await async_rs_or_single_client(maxPoolSize=1, retryReads=False) + collection = client.pymongo_test.test + pool = await async_get_pool(client) + pool._check_interval_seconds = None # Never check. + + # Ensure a socket. + await connected(client) + + # Cause a network error. + conn = one(pool.conns) + conn.conn.close() + cursor = collection.find(cursor_type=CursorType.EXHAUST) + with pytest.raises(ConnectionFailure): + await anext(cursor) + + assert conn.closed + + # The semaphore was decremented despite the error. + assert 0 == pool.requests + + @pytest.mark.usefixtures("require_auth") + async def test_auth_network_error(self, async_rs_or_single_client): + # Make sure there's no semaphore leak if we get a network error + # when authenticating a new socket with cached credentials. + + # Get a client with one socket so we detect if it's leaked. + c = await connected( + await async_rs_or_single_client( + maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False + ) + ) + + # Cause a network error on the actual socket. + pool = await async_get_pool(c) + conn = one(pool.conns) + conn.conn.close() + + # AsyncConnection.authenticate logs, but gets a socket.error. Should be + # reraised as AutoReconnect. + with pytest.raises(AutoReconnect): + await c.test.collection.find_one() + + # No semaphore leak, the pool is allowed to make a new socket. + await c.test.collection.find_one() + + @pytest.mark.usefixtures("require_no_replica_set") + async def test_connect_to_standalone_using_replica_set_name(self, async_single_client): + client = await async_single_client(replicaSet="anything", serverSelectionTimeoutMS=100) + with pytest.raises(AutoReconnect): + await client.test.test.find_one() + + @pytest.mark.usefixtures("require_replica_set") + async def test_stale_getmore(self, async_rs_client): + # A cursor is created, but its member goes down and is removed from + # the topology before the getMore message is sent. Test that + # AsyncMongoClient._run_operation_with_response handles the error. + with pytest.raises(AutoReconnect): + client = await async_rs_client(connect=False, serverSelectionTimeoutMS=100) + await client._run_operation( + operation=message._GetMore( + "pymongo_test", + "collection", + 101, + 1234, + client.codec_options, + ReadPreference.PRIMARY, + None, + client, + None, + None, + False, + None, + ), + unpack_res=AsyncCursor(client.pymongo_test.collection)._unpack_response, + address=("not-a-member", 27017), + ) + + async def test_heartbeat_frequency_ms(self, async_client_context_fixture, async_single_client): + class HeartbeatStartedListener(ServerHeartbeatListener): + def __init__(self): + self.results = [] + + def started(self, event): + self.results.append(event) + + def succeeded(self, event): + pass + + def failed(self, event): + pass + + old_init = ServerHeartbeatStartedEvent.__init__ + heartbeat_times = [] + + def init(self, *args): + old_init(self, *args) + heartbeat_times.append(time.time()) + + try: + ServerHeartbeatStartedEvent.__init__ = init # type: ignore + listener = HeartbeatStartedListener() + uri = "mongodb://%s:%d/?heartbeatFrequencyMS=500" % ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + await async_single_client(uri, event_listeners=[listener]) + await async_wait_until( + lambda: len(listener.results) >= 2, "record two ServerHeartbeatStartedEvents" + ) + + # Default heartbeatFrequencyMS is 10 sec. Check the interval was + # closer to 0.5 sec with heartbeatFrequencyMS configured. + pytest.approx(heartbeat_times[1] - heartbeat_times[0], 0.5, abs=2) + + finally: + ServerHeartbeatStartedEvent.__init__ = old_init # type: ignore + + async def test_small_heartbeat_frequency_ms(self): + uri = "mongodb://example/?heartbeatFrequencyMS=499" + with pytest.raises(ConfigurationError) as context: + AsyncMongoClient(uri) + + assert "heartbeatFrequencyMS" in str(context.value) + + + async def test_compression(self, async_client_context_fixture, simple_client, async_single_client): + def compression_settings(client): + pool_options = client.options.pool_options + return pool_options._compression_settings + + client = await simple_client("mongodb://localhost:27017/?compressors=zlib", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zlib"] + + client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=4", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == 4 + + client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-1", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017", connect=False) + opts = compression_settings(client) + assert opts.compressors == [] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017/?compressors=foobar", connect=False) + opts = compression_settings(client) + assert opts.compressors == [] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017/?compressors=foobar,zlib", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=10", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-2", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + if not _have_snappy(): + client = await simple_client("mongodb://localhost:27017/?compressors=snappy", connect=False) + opts = compression_settings(client) + assert opts.compressors == [] + else: + client = await simple_client("mongodb://localhost:27017/?compressors=snappy", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["snappy"] + client = await simple_client("mongodb://localhost:27017/?compressors=snappy,zlib", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["snappy", "zlib"] + + if not _have_zstd(): + client = await simple_client("mongodb://localhost:27017/?compressors=zstd", connect=False) + opts = compression_settings(client) + assert opts.compressors == [] + else: + client = await simple_client("mongodb://localhost:27017/?compressors=zstd", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zstd"] + client = await simple_client("mongodb://localhost:27017/?compressors=zstd,zlib", connect=False) + opts = compression_settings(client) + assert opts.compressors == ["zstd", "zlib"] + + options = async_client_context_fixture.default_client_options + if "compressors" in options and "zlib" in options["compressors"]: + for level in range(-1, 10): + client = await async_single_client(zlibcompressionlevel=level) + await client.pymongo_test.test.find_one() # No error + + @pytest.mark.usefixtures("require_sync") + async def test_reset_during_update_pool(self, async_rs_or_single_client): + client = await async_rs_or_single_client(minPoolSize=10) + await client.admin.command("ping") + pool = await async_get_pool(client) + generation = pool.gen.get_overall() + + # Continuously reset the pool. + class ResetPoolThread(threading.Thread): + def __init__(self, pool): + super().__init__() + self.running = True + self.pool = pool + + def stop(self): + self.running = False + + async def _run(self): + while self.running: + exc = AutoReconnect("mock pool error") + ctx = _ErrorContext(exc, 0, pool.gen.get_overall(), False, None) + await client._topology.handle_error(pool.address, ctx) + await asyncio.sleep(0.001) + + def run(self): + asyncio.run(self._run()) + + t = ResetPoolThread(pool) + t.start() + + # Ensure that update_pool completes without error even when the pool is reset concurrently. + try: + while True: + for _ in range(10): + await client._topology.update_pool() + if generation != pool.gen.get_overall(): + break + finally: + t.stop() + t.join() + await client.admin.command("ping") + + async def test_background_connections_do_not_hold_locks(self, async_rs_or_single_client): + min_pool_size = 10 + client = await async_rs_or_single_client(serverSelectionTimeoutMS=3000, minPoolSize=min_pool_size, + connect=False) + await client.admin.command("ping") # Create a single connection in the pool + + # Cause new connections to stall for a few seconds. + pool = await async_get_pool(client) + original_connect = pool.connect + + async def stall_connect(*args, **kwargs): + await asyncio.sleep(2) + return await original_connect(*args, **kwargs) + + pool.connect = stall_connect + # TODO + # self.addCleanup(delattr, pool, "connect") + + await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") + + # Assert that application operations do not block. + for _ in range(10): + start = time.monotonic() + await client.admin.command("ping") + total = time.monotonic() - start + assert total < 2 + + @pytest.mark.usefixtures("require_replica_set") + async def test_direct_connection(self, async_rs_or_single_client): + client = await async_rs_or_single_client(directConnection=True) + await client.admin.command("ping") + assert len(client.nodes) == 1 + assert client._topology_settings.get_topology_type() == TOPOLOGY_TYPE.Single + + client = await async_rs_or_single_client(directConnection=False) + await client.admin.command("ping") + assert len(client.nodes) >= 1 + assert client._topology_settings.get_topology_type() in [ + TOPOLOGY_TYPE.ReplicaSetNoPrimary, TOPOLOGY_TYPE.ReplicaSetWithPrimary + ] + + with pytest.raises(ConfigurationError): + AsyncMongoClient(["host1", "host2"], directConnection=True) + + @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2927 fails often on PyPy") + async def test_continuous_network_errors(self, simple_client): + def server_description_count(): + i = 0 + for obj in gc.get_objects(): + try: + if isinstance(obj, ServerDescription): + i += 1 + except ReferenceError: + pass + return i + + gc.collect() + with client_knobs(min_heartbeat_interval=0.003): + client = await simple_client("invalid:27017", heartbeatFrequencyMS=3, serverSelectionTimeoutMS=150) + initial_count = server_description_count() + with pytest.raises(ServerSelectionTimeoutError): + await client.test.test.find_one() + gc.collect() + final_count = server_description_count() + assert pytest.approx(initial_count, abs=20) == final_count + + @pytest.mark.usefixtures("require_failCommand_fail_point") + async def test_network_error_message(self, async_single_client): + client = await async_single_client(retryReads=False) + await client.admin.command("ping") # connect + async with self.fail_point(client, {"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}}): + assert await client.address is not None + expected = "{}:{}: ".format(*(await client.address)) + with pytest.raises(AutoReconnect, match=expected): + await client.pymongo_test.test.find_one({}) + + @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2938 could fail on PyPy") + async def test_process_periodic_tasks(self, async_rs_or_single_client): + client = await async_rs_or_single_client() + coll = client.db.collection + await coll.insert_many([{} for _ in range(5)]) + cursor = coll.find(batch_size=2) + await cursor.next() + c_id = cursor.cursor_id + assert c_id is not None + await client.close() + del cursor + await async_wait_until(lambda: client._kill_cursors_queue, "waited for cursor to be added to queue") + await client._process_periodic_tasks() # This must not raise or print any exceptions + with pytest.raises(InvalidOperation): + await coll.insert_many([{} for _ in range(5)]) + + async def test_service_name_from_kwargs(self): + client = AsyncMongoClient("mongodb+srv://user:password@test22.test.build.10gen.cc", srvServiceName="customname", + connect=False) + assert client._topology_settings.srv_service_name == "customname" + + client = AsyncMongoClient( + "mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=shouldbeoverriden", + srvServiceName="customname", connect=False) + assert client._topology_settings.srv_service_name == "customname" + + client = AsyncMongoClient("mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=customname", + connect=False) + assert client._topology_settings.srv_service_name == "customname" + + async def test_srv_max_hosts_kwarg(self, simple_client): + client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/") + assert len(client.topology_description.server_descriptions()) > 1 + + client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1) + assert len(client.topology_description.server_descriptions()) == 1 + + client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1", srvmaxhosts=2) + assert len(client.topology_description.server_descriptions()) == 2 + + @pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support SIGSTOP") + @pytest.mark.usefixtures("require_sdam") + @pytest.mark.usefixtures("require_sync") + def test_sigstop_sigcont(self, async_client_context): + test_dir = os.path.dirname(os.path.realpath(__file__)) + script = os.path.join(test_dir, "sigstop_sigcont.py") + p = subprocess.Popen( + [sys.executable, script, async_client_context.uri], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + # TODO + # self.addCleanup(p.wait, timeout=1) + # self.addCleanup(p.kill) + time.sleep(1) + os.kill(p.pid, signal.SIGSTOP) + time.sleep(2) + os.kill(p.pid, signal.SIGCONT) + time.sleep(0.5) + outs, _ = p.communicate(input=b"q\n", timeout=10) + assert outs + log_output = outs.decode("utf-8") + assert "TEST STARTED" in log_output + assert "ServerHeartbeatStartedEvent" in log_output + assert "ServerHeartbeatSucceededEvent" in log_output + assert "TEST COMPLETED" in log_output + assert "ServerHeartbeatFailedEvent" not in log_output + + async def _test_handshake(self, env_vars, expected_env, async_rs_or_single_client): + with patch.dict("os.environ", env_vars): + metadata = copy.deepcopy(_METADATA) + if has_c(): + metadata["driver"]["name"] = "PyMongo|c|async" + else: + metadata["driver"]["name"] = "PyMongo|async" + + if expected_env is not None: + metadata["env"] = expected_env + + if "AWS_REGION" not in env_vars: + os.environ["AWS_REGION"] = "" + + client = await async_rs_or_single_client(serverSelectionTimeoutMS=10000) + await client.admin.command("ping") + options = client.options + assert options.pool_options.metadata == metadata + + async def test_handshake_01_aws(self, async_rs_or_single_client): + await self._test_handshake( + { + "AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", + "AWS_REGION": "us-east-2", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + }, + {"name": "aws.lambda", "region": "us-east-2", "memory_mb": 1024}, + async_rs_or_single_client, + ) + + async def test_handshake_02_azure(self, async_rs_or_single_client): + await self._test_handshake( + {"FUNCTIONS_WORKER_RUNTIME": "python"}, {"name": "azure.func"}, async_rs_or_single_client + ) + + async def test_handshake_03_gcp(self, async_rs_or_single_client): + # Regular case with environment variables. + await self._test_handshake( + { + "K_SERVICE": "servicename", + "FUNCTION_MEMORY_MB": "1024", + "FUNCTION_TIMEOUT_SEC": "60", + "FUNCTION_REGION": "us-central1", + }, + {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, + async_rs_or_single_client, + ) + + # Extra case for FUNCTION_NAME. + await self._test_handshake( + { + "FUNCTION_NAME": "funcname", + "FUNCTION_MEMORY_MB": "1024", + "FUNCTION_TIMEOUT_SEC": "60", + "FUNCTION_REGION": "us-central1", + }, + {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, + async_rs_or_single_client, + ) + + async def test_handshake_04_vercel(self, async_rs_or_single_client): + await self._test_handshake( + {"VERCEL": "1", "VERCEL_REGION": "cdg1"}, {"name": "vercel", "region": "cdg1"}, async_rs_or_single_client + ) + + async def test_handshake_05_multiple(self, async_rs_or_single_client): + await self._test_handshake( + {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "FUNCTIONS_WORKER_RUNTIME": "python"}, + None, + async_rs_or_single_client, + ) + + await self._test_handshake( + {"FUNCTIONS_WORKER_RUNTIME": "python", "K_SERVICE": "servicename"}, None, async_rs_or_single_client + ) + + await self._test_handshake({"K_SERVICE": "servicename", "VERCEL": "1"}, None, async_rs_or_single_client) + + async def test_handshake_06_region_too_long(self, async_rs_or_single_client): + await self._test_handshake( + {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_REGION": "a" * 512}, + {"name": "aws.lambda"}, + async_rs_or_single_client, + ) + + async def test_handshake_07_memory_invalid_int(self, async_rs_or_single_client): + await self._test_handshake( + {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big"}, + {"name": "aws.lambda"}, + async_rs_or_single_client, + ) + + async def test_handshake_08_invalid_aws_ec2(self, async_rs_or_single_client): + # AWS_EXECUTION_ENV needs to start with "AWS_Lambda_". + await self._test_handshake({"AWS_EXECUTION_ENV": "EC2"}, None, async_rs_or_single_client) + + async def test_handshake_09_container_with_provider(self, async_rs_or_single_client): + await self._test_handshake( + { + ENV_VAR_K8S: "1", + "AWS_LAMBDA_RUNTIME_API": "1", + "AWS_REGION": "us-east-1", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "256", + }, + { + "container": {"orchestrator": "kubernetes"}, + "name": "aws.lambda", + "region": "us-east-1", + "memory_mb": 256, + }, + async_rs_or_single_client, + ) + + async def test_dict_hints(self, async_client): + async_client.db.t.find(hint={"x": 1}) + + async def test_dict_hints_sort(self, async_client): + result = async_client.db.t.find() + result.sort({"x": 1}) + async_client.db.t.find(sort={"x": 1}) + + async def test_dict_hints_create_index(self, async_client): + await async_client.db.t.create_index({"x": pymongo.ASCENDING}) + + async def test_legacy_java_uuid_roundtrip(self, async_client_context_fixture): + data = BinaryData.java_data + docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, JAVA_LEGACY)) + + await async_client_context_fixture.client.pymongo_test.drop_collection("java_uuid") + db = async_client_context_fixture.client.pymongo_test + coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=JAVA_LEGACY)) + + await coll.insert_many(docs) + assert await coll.count_documents({}) == 5 + async for d in coll.find(): + assert d["newguid"] == uuid.UUID(d["newguidstring"]) + + coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) + async for d in coll.find(): + assert d["newguid"] != d["newguidstring"] + await async_client_context_fixture.client.pymongo_test.drop_collection("java_uuid") + + async def test_legacy_csharp_uuid_roundtrip(self, async_client_context_fixture): + data = BinaryData.csharp_data + docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, CSHARP_LEGACY)) + + await async_client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") + db = async_client_context_fixture.client.pymongo_test + coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=CSHARP_LEGACY)) + + await coll.insert_many(docs) + assert await coll.count_documents({}) == 5 + async for d in coll.find(): + assert d["newguid"] == uuid.UUID(d["newguidstring"]) + + coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) + async for d in coll.find(): + assert d["newguid"] != d["newguidstring"] + await async_client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") + + async def test_uri_to_uuid(self, async_single_client): + uri = "mongodb://foo/?uuidrepresentation=csharpLegacy" + client = await async_single_client(uri, connect=False) + assert client.pymongo_test.test.codec_options.uuid_representation == CSHARP_LEGACY + + async def test_uuid_queries(self, async_client_context_fixture): + db = async_client_context_fixture.client.pymongo_test + coll = db.test + await coll.drop() + + uu = uuid.uuid4() + await coll.insert_one({"uuid": Binary(uu.bytes, 3)}) + assert await coll.count_documents({}) == 1 + + coll = db.get_collection("test", CodecOptions(uuid_representation=UuidRepresentation.STANDARD)) + assert await coll.count_documents({"uuid": uu}) == 0 + await coll.insert_one({"uuid": uu}) + assert await coll.count_documents({}) == 2 + docs = await coll.find({"uuid": uu}).to_list(length=1) + assert len(docs) == 1 + assert docs[0]["uuid"] == uu + + uu_legacy = Binary.from_uuid(uu, UuidRepresentation.PYTHON_LEGACY) + predicate = {"uuid": {"$in": [uu, uu_legacy]}} + assert await coll.count_documents(predicate) == 2 + docs = await coll.find(predicate).to_list(length=2) + assert len(docs) == 2 + await coll.drop() From ae650e097d5641743511fd890ed147e31373becd Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 22 Jan 2025 13:27:43 -0500 Subject: [PATCH 07/29] Asynchronous test_client.py done --- test/asynchronous/__init__.py | 7 +- test/asynchronous/conftest.py | 54 ++- test/asynchronous/test_client_pytest.py | 449 +++++++++++++++++++++++- 3 files changed, 482 insertions(+), 28 deletions(-) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index e93f023305..2a69c0572c 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -15,24 +15,19 @@ """Asynchronous test suite for pymongo, bson, and gridfs.""" from __future__ import annotations -import asyncio import gc -import logging import multiprocessing import os import signal import socket import subprocess import sys -import threading import time -import traceback import unittest import warnings from asyncio import iscoroutinefunction -import pytest_asyncio -from pymongo.lock import _create_lock, _async_create_lock +from pymongo.lock import _async_create_lock from test.helpers import ( COMPRESSORS, diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index 22fd839d17..dbb52a5c76 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import contextlib import sys from typing import Callable @@ -17,6 +16,7 @@ import pytest import pytest_asyncio +from test.asynchronous.pymongo_mocks import AsyncMockClient from test.utils import FunctionCallRecorder _IS_SYNC = False @@ -157,11 +157,17 @@ async def _async_mongo_client( return client -async def async_single_client_noauth( - async_client_context, h: Any = None, p: Any = None, **kwargs: Any -) -> AsyncMongoClient[dict]: +@pytest_asyncio.fixture(loop_scope="session") +async def async_single_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: """Make a direct connection. Don't authenticate.""" - return await _async_mongo_client(async_client_context, h, p, authenticate=False, directConnection=True, **kwargs) + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = await _async_mongo_client(async_client_context_fixture, h, p, authenticate=False, directConnection=True, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() @pytest_asyncio.fixture(loop_scope="session") async def async_single_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: @@ -175,13 +181,18 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): for client in clients: await client.close() -# @pytest_asyncio.fixture(loop_scope="function") -# async def async_rs_client_noauth( -# async_client_context, h: Any = None, p: Any = None, **kwargs: Any -# ) -> AsyncMongoClient[dict]: -# """Connect to the replica set. Don't authenticate.""" -# return await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) -# +@pytest_asyncio.fixture(loop_scope="session") +async def async_rs_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: + """Connect to the replica set. Don't authenticate.""" + clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = await _async_mongo_client(async_client_context_fixture, h, p, authenticate=False, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() + @pytest_asyncio.fixture(loop_scope="session") async def async_rs_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: @@ -250,6 +261,23 @@ def patch_resolver(): yield patched_resolver pymongo.srv_resolver._resolve = _resolve - +@pytest_asyncio.fixture(loop_scope="session") +async def async_mock_client(): + clients = [] + + async def _make_client(standalones, + members, + mongoses, + hello_hosts=None, + arbiters=None, + down_hosts=None, + *args, + **kwargs): + client = await AsyncMockClient.get_async_mock_client(standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs) + clients.append(client) + return client + yield _make_client + for client in clients: + await client.close() pytest_collection_modifyitems = pytest_conf.pytest_collection_modifyitems diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py index 17c39861e9..20c15b0fee 100644 --- a/test/asynchronous/test_client_pytest.py +++ b/test/asynchronous/test_client_pytest.py @@ -17,7 +17,6 @@ import _thread as thread import asyncio -import base64 import contextlib import copy import datetime @@ -39,7 +38,6 @@ import pytest import pytest_asyncio -from pymongo.lock import _async_create_lock from bson.binary import CSHARP_LEGACY, JAVA_LEGACY, PYTHON_LEGACY, Binary, UuidRepresentation from pymongo.operations import _Op @@ -50,23 +48,17 @@ from test.asynchronous import ( HAVE_IPADDRESS, - AsyncIntegrationTest, - AsyncMockClientTest, - AsyncUnitTest, SkipTest, client_knobs, connected, db_pwd, db_user, - remove_all_users, - unittest, AsyncClientContext, AsyncPyMongoTestCasePyTest, + AsyncPyMongoTestCasePyTest, ) -from test.asynchronous.pymongo_mocks import AsyncMockClient from test.test_binary import BinaryData from test.utils import ( NTHREADS, CMAPListener, - FunctionCallRecorder, async_get_pool, async_wait_until, asyncAssertRaisesExactly, @@ -2097,3 +2089,442 @@ async def test_uuid_queries(self, async_client_context_fixture): docs = await coll.find(predicate).to_list(length=2) assert len(docs) == 2 await coll.drop() + +@pytest.mark.usefixtures("require_no_mongos") +class TestAsyncExhaustCursor(AsyncPyMongoTestCasePyTest): + async def test_exhaust_query_server_error(self, async_rs_or_single_client): + # When doing an exhaust query, the socket stays checked out on success + # but must be checked in on error to avoid semaphore leaks. + client = await connected(await async_rs_or_single_client(maxPoolSize=1)) + + collection = client.pymongo_test.test + pool = await async_get_pool(client) + conn = one(pool.conns) + + # This will cause OperationFailure in all mongo versions since + # the value for $orderby must be a document. + cursor = collection.find( + SON([("$query", {}), ("$orderby", True)]), cursor_type=CursorType.EXHAUST + ) + + with pytest.raises(OperationFailure): + await cursor.next() + assert not conn.closed + + # The socket was checked in and the semaphore was decremented. + assert conn in pool.conns + assert pool.requests == 0 + + async def test_exhaust_getmore_server_error(self, async_rs_or_single_client): + # When doing a getmore on an exhaust cursor, the socket stays checked + # out on success but it's checked in on error to avoid semaphore leaks. + client = await async_rs_or_single_client(maxPoolSize=1) + collection = client.pymongo_test.test + await collection.drop() + + await collection.insert_many([{} for _ in range(200)]) + + pool = await async_get_pool(client) + pool._check_interval_seconds = None # Never check. + conn = one(pool.conns) + + cursor = collection.find(cursor_type=CursorType.EXHAUST) + + # Initial query succeeds. + await cursor.next() + + # Cause a server error on getmore. + async def receive_message(request_id): + # Discard the actual server response. + await AsyncConnection.receive_message(conn, request_id) + + # responseFlags bit 1 is QueryFailure. + msg = struct.pack(" Date: Wed, 22 Jan 2025 15:51:49 -0500 Subject: [PATCH 08/29] Fix fixture scopes --- test/asynchronous/conftest.py | 6 +++--- test/asynchronous/test_client_pytest.py | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index dbb52a5c76..4a09fc87d1 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -32,14 +32,14 @@ def event_loop_policy(): return asyncio.get_event_loop_policy() -@pytest_asyncio.fixture(loop_scope="session") +@pytest_asyncio.fixture(loop_scope="session", scope="session") async def async_client_context_fixture(): client = AsyncClientContext() await client.init() yield client await client.client.close() -@pytest_asyncio.fixture(loop_scope="session", autouse=True) +@pytest_asyncio.fixture(loop_scope="session", scope="session") async def test_environment(async_client_context_fixture): requirements = {} requirements["SUPPORT_TRANSACTIONS"] = async_client_context_fixture.supports_transactions() @@ -119,7 +119,7 @@ async def require_failCommand_fail_point(test_environment): pytest.skip("failCommand fail point must be supported") -@pytest_asyncio.fixture(loop_scope="session", autouse=True) +@pytest_asyncio.fixture(loop_scope="session", scope="session", autouse=True) async def test_setup_and_teardown(): await async_setup() yield diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py index 20c15b0fee..db3b647560 100644 --- a/test/asynchronous/test_client_pytest.py +++ b/test/asynchronous/test_client_pytest.py @@ -41,8 +41,6 @@ from bson.binary import CSHARP_LEGACY, JAVA_LEGACY, PYTHON_LEGACY, Binary, UuidRepresentation from pymongo.operations import _Op -from test.asynchronous.conftest import simple_client, async_single_client, async_rs_client, \ - async_rs_or_single_client sys.path[0:0] = [""] From 048edf21d30a931d1147c26c94037a5295467663 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 10:58:32 -0500 Subject: [PATCH 09/29] test_client.py conversion complete --- test/asynchronous/__init__.py | 4 +- test/asynchronous/conftest.py | 120 +++++---- test/asynchronous/test_client_pytest.py | 327 ++++++++++++------------ 3 files changed, 233 insertions(+), 218 deletions(-) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 2a69c0572c..28fa6f434e 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -26,9 +26,6 @@ import unittest import warnings from asyncio import iscoroutinefunction - -from pymongo.lock import _async_create_lock - from test.helpers import ( COMPRESSORS, IS_SRV, @@ -52,6 +49,7 @@ sanitize_reply, ) +from pymongo.lock import _async_create_lock from pymongo.uri_parser import parse_uri try: diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index 4a09fc87d1..f123f8d37f 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -2,23 +2,27 @@ import asyncio import sys +from test import MONGODB_API_VERSION, db_pwd, db_user, pytest_conf +from test.asynchronous import ( + AsyncClientContext, + _connection_string, + async_setup, + async_teardown, + remove_all_users, +) +from test.asynchronous.pymongo_mocks import AsyncMockClient +from test.utils import FunctionCallRecorder from typing import Callable -import pymongo +import pytest +import pytest_asyncio from typing_extensions import Any +import pymongo from pymongo import AsyncMongoClient +from pymongo.asynchronous.database import AsyncDatabase from pymongo.uri_parser import parse_uri -from test import pytest_conf, db_user, db_pwd, MONGODB_API_VERSION -from test.asynchronous import async_setup, async_teardown, _connection_string, AsyncClientContext - -import pytest -import pytest_asyncio - -from test.asynchronous.pymongo_mocks import AsyncMockClient -from test.utils import FunctionCallRecorder - _IS_SYNC = False @@ -33,38 +37,45 @@ def event_loop_policy(): return asyncio.get_event_loop_policy() @pytest_asyncio.fixture(loop_scope="session", scope="session") -async def async_client_context_fixture(): +async def async_client_context(): client = AsyncClientContext() await client.init() yield client - await client.client.close() + if client.client is not None: + await client.client.close() + + +@pytest_asyncio.fixture +async def integration_test(async_client_context): + if not async_client_context.connected: + pytest.fail("Integration tests require a MongoDB server") @pytest_asyncio.fixture(loop_scope="session", scope="session") -async def test_environment(async_client_context_fixture): +async def test_environment(async_client_context): requirements = {} - requirements["SUPPORT_TRANSACTIONS"] = async_client_context_fixture.supports_transactions() - requirements["IS_DATA_LAKE"] = async_client_context_fixture.is_data_lake + requirements["SUPPORT_TRANSACTIONS"] = async_client_context.supports_transactions() + requirements["IS_DATA_LAKE"] = async_client_context.is_data_lake requirements["IS_SYNC"] = _IS_SYNC requirements["IS_SYNC"] = _IS_SYNC requirements["REQUIRE_API_VERSION"] = MONGODB_API_VERSION - requirements["SUPPORTS_FAILCOMMAND_FAIL_POINT"] = async_client_context_fixture.supports_failCommand_fail_point - requirements["IS_NOT_MMAP"] = async_client_context_fixture.is_not_mmap - requirements["SERVER_VERSION"] = async_client_context_fixture.version - requirements["AUTH_ENABLED"] = async_client_context_fixture.auth_enabled - requirements["FIPS_ENABLED"] = async_client_context_fixture.fips_enabled - requirements["IS_RS"] = async_client_context_fixture.is_rs - requirements["MONGOSES"] = len(async_client_context_fixture.mongoses) - requirements["SECONDARIES_COUNT"] = await async_client_context_fixture.secondaries_count - requirements["SECONDARY_READ_PREF"] = await async_client_context_fixture.supports_secondary_read_pref - requirements["HAS_IPV6"] = async_client_context_fixture.has_ipv6 - requirements["IS_SERVERLESS"] = async_client_context_fixture.serverless - requirements["IS_LOAD_BALANCER"] = async_client_context_fixture.load_balancer - requirements["TEST_COMMANDS_ENABLED"] = async_client_context_fixture.test_commands_enabled - requirements["IS_TLS"] = async_client_context_fixture.tls - requirements["IS_TLS_CERT"] = async_client_context_fixture.tlsCertificateKeyFile - requirements["SERVER_IS_RESOLVEABLE"] = async_client_context_fixture.server_is_resolvable - requirements["SESSIONS_ENABLED"] = async_client_context_fixture.sessions_enabled - requirements["SUPPORTS_RETRYABLE_WRITES"] = async_client_context_fixture.supports_retryable_writes() + requirements["SUPPORTS_FAILCOMMAND_FAIL_POINT"] = async_client_context.supports_failCommand_fail_point + requirements["IS_NOT_MMAP"] = async_client_context.is_not_mmap + requirements["SERVER_VERSION"] = async_client_context.version + requirements["AUTH_ENABLED"] = async_client_context.auth_enabled + requirements["FIPS_ENABLED"] = async_client_context.fips_enabled + requirements["IS_RS"] = async_client_context.is_rs + requirements["MONGOSES"] = len(async_client_context.mongoses) + requirements["SECONDARIES_COUNT"] = await async_client_context.secondaries_count + requirements["SECONDARY_READ_PREF"] = await async_client_context.supports_secondary_read_pref + requirements["HAS_IPV6"] = async_client_context.has_ipv6 + requirements["IS_SERVERLESS"] = async_client_context.serverless + requirements["IS_LOAD_BALANCER"] = async_client_context.load_balancer + requirements["TEST_COMMANDS_ENABLED"] = async_client_context.test_commands_enabled + requirements["IS_TLS"] = async_client_context.tls + requirements["IS_TLS_CERT"] = async_client_context.tlsCertificateKeyFile + requirements["SERVER_IS_RESOLVEABLE"] = async_client_context.server_is_resolvable + requirements["SESSIONS_ENABLED"] = async_client_context.sessions_enabled + requirements["SUPPORTS_RETRYABLE_WRITES"] = async_client_context.supports_retryable_writes() yield requirements @@ -158,11 +169,11 @@ async def _async_mongo_client( @pytest_asyncio.fixture(loop_scope="session") -async def async_single_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: +async def async_single_client_noauth(async_client_context) -> Callable[..., AsyncMongoClient]: """Make a direct connection. Don't authenticate.""" clients = [] async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context_fixture, h, p, authenticate=False, directConnection=True, **kwargs) + client = await _async_mongo_client(async_client_context, h, p, authenticate=False, directConnection=True, **kwargs) clients.append(client) return client yield _make_client @@ -170,11 +181,11 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): await client.close() @pytest_asyncio.fixture(loop_scope="session") -async def async_single_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: +async def async_single_client(async_client_context) -> Callable[..., AsyncMongoClient]: """Make a direct connection, and authenticate if necessary.""" clients = [] async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context_fixture, h, p, directConnection=True, **kwargs) + client = await _async_mongo_client(async_client_context, h, p, directConnection=True, **kwargs) clients.append(client) return client yield _make_client @@ -182,11 +193,11 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): await client.close() @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: +async def async_rs_client_noauth(async_client_context) -> Callable[..., AsyncMongoClient]: """Connect to the replica set. Don't authenticate.""" clients = [] async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context_fixture, h, p, authenticate=False, **kwargs) + client = await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) clients.append(client) return client yield _make_client @@ -195,11 +206,11 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: +async def async_rs_client(async_client_context) -> Callable[..., AsyncMongoClient]: """Connect to the replica set and authenticate if necessary.""" clients = [] async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context_fixture, h, p, **kwargs) + client = await _async_mongo_client(async_client_context, h, p, **kwargs) clients.append(client) return client yield _make_client @@ -208,14 +219,14 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_or_single_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: +async def async_rs_or_single_client_noauth(async_client_context) -> Callable[..., AsyncMongoClient]: """Connect to the replica set if there is one, otherwise the standalone. Like rs_or_single_client, but does not authenticate. """ clients = [] async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context_fixture, h, p, authenticate=False, **kwargs) + client = await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) clients.append(client) return client yield _make_client @@ -223,14 +234,14 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): await client.close() @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_or_single_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: +async def async_rs_or_single_client(async_client_context) -> Callable[..., AsyncMongoClient]: """Connect to the replica set if there is one, otherwise the standalone. Authenticates if necessary. """ clients = [] async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context_fixture, h, p, **kwargs) + client = await _async_mongo_client(async_client_context, h, p, **kwargs) clients.append(client) return client yield _make_client @@ -280,4 +291,23 @@ async def _make_client(standalones, for client in clients: await client.close() +@pytest_asyncio.fixture(loop_scope="session") +async def remove_all_users_fixture(async_client_context, request): + db_name = request.param + yield + await async_client_context.client[db_name].command("dropAllUsersFromDatabase", 1, writeConcern={"w": async_client_context.w}) + +@pytest_asyncio.fixture(loop_scope="session") +async def drop_user_fixture(async_client_context, request): + db, user = request.param + yield + await async_client_context.drop_user(db, user) + +@pytest_asyncio.fixture(loop_scope="session") +async def drop_database_fixture(async_client_context, request): + db = request.param + yield + await async_client_context.client.drop_database(db) + + pytest_collection_modifyitems = pytest_conf.pytest_collection_modifyitems diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py index db3b647560..2c9e7d9a35 100644 --- a/test/asynchronous/test_client_pytest.py +++ b/test/asynchronous/test_client_pytest.py @@ -46,12 +46,12 @@ from test.asynchronous import ( HAVE_IPADDRESS, + AsyncPyMongoTestCasePyTest, SkipTest, client_knobs, connected, db_pwd, db_user, - AsyncPyMongoTestCasePyTest, ) from test.test_binary import BinaryData from test.utils import ( @@ -268,11 +268,10 @@ async def test_iteration(self, async_client): assert not isinstance(async_client, Iterable) - - async def test_get_default_database(self, async_rs_or_single_client, async_client_context_fixture): + async def test_get_default_database(self, async_rs_or_single_client, async_client_context): c = await async_rs_or_single_client( "mongodb://%s:%d/foo" - % (await async_client_context_fixture.host, await async_client_context_fixture.port), + % (await async_client_context.host, await async_client_context.port), connect=False, ) assert AsyncDatabase(c, "foo") == c.get_default_database() @@ -287,52 +286,52 @@ async def test_get_default_database(self, async_rs_or_single_client, async_clien assert write_concern == db.write_concern c = await async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), connect=False, ) assert AsyncDatabase(c, "foo") == c.get_default_database("foo") - async def test_get_default_database_error(self, async_rs_or_single_client, async_client_context_fixture): + async def test_get_default_database_error(self, async_rs_or_single_client, async_client_context): # URI with no database. c = await async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), connect=False, ) with pytest.raises(ConfigurationError): c.get_default_database() - async def test_get_default_database_with_authsource(self, async_client_context_fixture, async_rs_or_single_client): + async def test_get_default_database_with_authsource(self, async_client_context, async_rs_or_single_client): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context_fixture.host, - await async_client_context_fixture.port, + await async_client_context.host, + await async_client_context.port, ) c = await async_rs_or_single_client(uri, connect=False) assert (AsyncDatabase(c, "foo") == c.get_default_database()) - async def test_get_database_default(self, async_client_context_fixture, async_rs_or_single_client): + async def test_get_database_default(self, async_client_context, async_rs_or_single_client): c = await async_rs_or_single_client( "mongodb://%s:%d/foo" - % (await async_client_context_fixture.host, await async_client_context_fixture.port), + % (await async_client_context.host, await async_client_context.port), connect=False, ) assert AsyncDatabase(c, "foo") == c.get_database() - async def test_get_database_default_error(self, async_client_context_fixture, async_rs_or_single_client): + async def test_get_database_default_error(self, async_client_context, async_rs_or_single_client): # URI with no database. c = await async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context_fixture.host, await async_client_context_fixture.port), + "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), connect=False, ) with pytest.raises(ConfigurationError): c.get_database() - async def test_get_database_default_with_authsource(self, async_client_context_fixture, async_rs_or_single_client): + async def test_get_database_default_with_authsource(self, async_client_context, async_rs_or_single_client): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context_fixture.host, - await async_client_context_fixture.port, + await async_client_context.host, + await async_client_context.port, ) c = await async_rs_or_single_client(uri, connect=False) assert AsyncDatabase(c, "foo") == c.get_database() @@ -348,7 +347,7 @@ async def test_primary_read_pref_with_tags(self, async_single_client): ): pass - async def test_read_preference(self, async_client_context_fixture, async_rs_or_single_client): + async def test_read_preference(self, async_client_context, async_rs_or_single_client): c = await async_rs_or_single_client( "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode ) @@ -484,7 +483,7 @@ def transform_python(self, value): assert c.codec_options.tzinfo == tzinfo - async def test_uri_codec_options(self, async_client_context_fixture, simple_client): + async def test_uri_codec_options(self, async_client_context, simple_client): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" datetime_conversion = "DATETIME_CLAMP" @@ -493,8 +492,8 @@ async def test_uri_codec_options(self, async_client_context_fixture, simple_clie "%s&unicode_decode_error_handler=%s" "&datetime_conversion=%s" % ( - await async_client_context_fixture.host, - await async_client_context_fixture.port, + await async_client_context.host, + await async_client_context.port, uuid_representation_label, unicode_decode_error_handler, datetime_conversion, @@ -653,14 +652,8 @@ async def test_detected_environment_warning(self, caplog, simple_client): await simple_client(multi_host) +@pytest.mark.usefixtures("integration_test") class TestAsyncClientIntegrationTest(AsyncPyMongoTestCasePyTest): - @pytest_asyncio.fixture(loop_scope="session") - async def async_client(self, async_rs_or_single_client) -> AsyncMongoClient: - client = await async_rs_or_single_client( - connect=False, serverSelectionTimeoutMS=100 - ) - yield client - await client.close() async def test_multiple_uris(self): with pytest.raises(ConfigurationError): @@ -792,15 +785,15 @@ async def test_max_idle_time_checkout(self, async_rs_or_single_client): assert conn == new_conn assert len(server._pool.conns) == 1 - async def test_constants(self, async_client_context_fixture, simple_client): + async def test_constants(self, async_client_context, simple_client): """This test uses AsyncMongoClient explicitly to make sure that host and port are not overloaded. """ - host, port = await async_client_context_fixture.host, await async_client_context_fixture.port - kwargs: dict = async_client_context_fixture.default_client_options.copy() - if async_client_context_fixture.auth_enabled: - kwargs["username"] = "user" # TODO: Replace with correctly managed auth creds - kwargs["password"] = "password" + host, port = await async_client_context.host, await async_client_context.port + kwargs: dict = async_client_context.default_client_options.copy() + if async_client_context.auth_enabled: + kwargs["username"] = db_user + kwargs["password"] = db_pwd # Set bad defaults. AsyncMongoClient.HOST = "somedomainthatdoesntexist.org" @@ -818,8 +811,8 @@ async def test_constants(self, async_client_context_fixture, simple_client): c = await simple_client(**kwargs) await connected(c) - async def test_init_disconnected(self, async_client_context_fixture, async_rs_or_single_client, simple_client): - host, port = await async_client_context_fixture.host, await async_client_context_fixture.port + async def test_init_disconnected(self, async_client_context, async_rs_or_single_client, simple_client): + host, port = await async_client_context.host, await async_client_context.port c = await async_rs_or_single_client(connect=False) # is_primary causes client to block until connected assert isinstance(await c.is_primary, bool) @@ -837,7 +830,7 @@ async def test_init_disconnected(self, async_client_context_fixture, async_rs_or c = await async_rs_or_single_client(connect=False) assert isinstance(c.topology_description, TopologyDescription) assert c.topology_description == c._topology._description - if async_client_context_fixture.is_rs: + if async_client_context.is_rs: # The primary's host and port are from the replica set config. assert await c.address is not None else: @@ -853,16 +846,16 @@ async def test_init_disconnected_with_auth(self, simple_client): with pytest.raises(ConnectionFailure): await c.pymongo_test.test.find_one() - async def test_equality(self, async_client_context_fixture, async_client, async_rs_or_single_client, simple_client): - seed = "{}:{}".format(*list(async_client._topology_settings.seeds)[0]) + async def test_equality(self, async_client_context, async_rs_or_single_client, simple_client): + seed = "{}:{}".format(*list(async_client_context.client._topology_settings.seeds)[0]) c = await async_rs_or_single_client(seed, connect=False) - assert async_client_context_fixture.client == c + assert async_client_context.client == c # Explicitly test inequality - assert not async_client_context_fixture.client != c + assert not async_client_context.client != c c = await async_rs_or_single_client("invalid.com", connect=False) - assert async_client_context_fixture.client != c - assert async_client_context_fixture.client != c + assert async_client_context.client != c + assert async_client_context.client != c c1 = await simple_client("a", connect=False) c2 = await simple_client("b", connect=False) @@ -876,16 +869,16 @@ async def test_equality(self, async_client_context_fixture, async_client, async_ # Same seeds but out of order still compares equal: assert c1 == c2 - async def test_hashable(self, async_client_context_fixture, async_client, async_rs_or_single_client): - seed = "{}:{}".format(*list(async_client._topology_settings.seeds)[0]) + async def test_hashable(self, async_client_context, async_rs_or_single_client): + seed = "{}:{}".format(*list(async_client_context.client._topology_settings.seeds)[0]) c = await async_rs_or_single_client(seed, connect=False) - assert c in {async_client_context_fixture.client} + assert c in {async_client_context.client} c = await async_rs_or_single_client("invalid.com", connect=False) - assert c not in {async_client_context_fixture.client} + assert c not in {async_client_context.client} - async def test_host_w_port(self, async_client_context_fixture): + async def test_host_w_port(self, async_client_context): with pytest.raises(ValueError): - host = await async_client_context_fixture.host + host = await async_client_context.host await connected( AsyncMongoClient( f"{host}:1234567", @@ -933,14 +926,14 @@ async def test_repr(self, simple_client): async with eval(the_repr) as client_two: assert client_two == client - # async def test_getters(self, async_client, async_client_context_fixture): - # await async_wait_until( - # lambda: async_client_context_fixture.nodes == async_client.nodes, "find all nodes" - # ) + async def test_getters(self, async_client_context): + await async_wait_until( + lambda: async_client_context.nodes == async_client_context.client.nodes, "find all nodes" + ) - async def test_list_databases(self, async_client, async_rs_or_single_client): - cmd_docs = (await async_client.admin.command("listDatabases"))["databases"] - cursor = await async_client.list_databases() + async def test_list_databases(self, async_client_context, async_rs_or_single_client): + cmd_docs = (await async_client_context.client.admin.command("listDatabases"))["databases"] + cursor = await async_client_context.client.list_databases() assert isinstance(cursor, AsyncCommandCursor) helper_docs = await cursor.to_list() assert len(helper_docs) > 0 @@ -954,47 +947,47 @@ async def test_list_databases(self, async_client, async_rs_or_single_client): async for doc in await client_doc.list_databases(): assert isinstance(doc, dict) - await async_client.pymongo_test.test.insert_one({}) - cursor = await async_client.list_databases(filter={"name": "admin"}) + await async_client_context.client.pymongo_test.test.insert_one({}) + cursor = await async_client_context.client.list_databases(filter={"name": "admin"}) docs = await cursor.to_list() assert len(docs) == 1 assert docs[0]["name"] == "admin" - cursor = await async_client.list_databases(nameOnly=True) + cursor = await async_client_context.client.list_databases(nameOnly=True) async for doc in cursor: assert list(doc) == ["name"] - async def test_list_database_names(self, async_client): - await async_client.pymongo_test.test.insert_one({"dummy": "object"}) - await async_client.pymongo_test_mike.test.insert_one({"dummy": "object"}) - cmd_docs = (await async_client.admin.command("listDatabases"))["databases"] + async def test_list_database_names(self, async_client_context): + await async_client_context.client.pymongo_test.test.insert_one({"dummy": "object"}) + await async_client_context.client.pymongo_test_mike.test.insert_one({"dummy": "object"}) + cmd_docs = (await async_client_context.client.admin.command("listDatabases"))["databases"] cmd_names = [doc["name"] for doc in cmd_docs] - db_names = await async_client.list_database_names() + db_names = await async_client_context.client.list_database_names() assert "pymongo_test" in db_names assert "pymongo_test_mike" in db_names assert db_names == cmd_names - async def test_drop_database(self, async_client_context_fixture, async_client, async_rs_or_single_client): + async def test_drop_database(self, async_client_context, async_rs_or_single_client): with pytest.raises(TypeError): - await async_client.drop_database(5) # type: ignore[arg-type] + await async_client_context.client.drop_database(5) # type: ignore[arg-type] with pytest.raises(TypeError): - await async_client.drop_database(None) # type: ignore[arg-type] + await async_client_context.client.drop_database(None) # type: ignore[arg-type] - await async_client.pymongo_test.test.insert_one({"dummy": "object"}) - await async_client.pymongo_test2.test.insert_one({"dummy": "object"}) - dbs = await async_client.list_database_names() + await async_client_context.client.pymongo_test.test.insert_one({"dummy": "object"}) + await async_client_context.client.pymongo_test2.test.insert_one({"dummy": "object"}) + dbs = await async_client_context.client.list_database_names() assert "pymongo_test" in dbs assert "pymongo_test2" in dbs - await async_client.drop_database("pymongo_test") + await async_client_context.client.drop_database("pymongo_test") - if async_client_context_fixture.is_rs: - wc_client = await async_rs_or_single_client(w=len(async_client_context_fixture.nodes) + 1) + if async_client_context.is_rs: + wc_client = await async_rs_or_single_client(w=len(async_client_context.nodes) + 1) with pytest.raises(WriteConcernError): await wc_client.drop_database("pymongo_test2") - await async_client.drop_database(async_client.pymongo_test2) - dbs = await async_client.list_database_names() + await async_client_context.client.drop_database(async_client_context.client.pymongo_test2) + dbs = await async_client_context.client.list_database_names() assert "pymongo_test" not in dbs assert "pymongo_test2" not in dbs @@ -1103,14 +1096,13 @@ async def test_bad_uri(self): @pytest.mark.usefixtures("require_auth") @pytest.mark.usefixtures("require_no_fips") - async def test_auth_from_uri(self, async_client_context_fixture, async_rs_or_single_client_noauth): - host, port = await async_client_context_fixture.host, await async_client_context_fixture.port - await async_client_context_fixture.create_user("admin", "admin", "pass") - # TODO - # self.addAsyncCleanup(async_client_context.drop_user, "admin", "admin") - # self.addAsyncCleanup(remove_all_users, self.client.pymongo_test) - - await async_client_context_fixture.create_user( + @pytest.mark.parametrize("remove_all_users_fixture", ["pymongo_test"], indirect=True) + @pytest.mark.parametrize("drop_user_fixture", [("admin", "admin")], indirect=True) + async def test_auth_from_uri(self, async_client_context, async_rs_or_single_client_noauth, remove_all_users_fixture, drop_user_fixture): + host, port = await async_client_context.host, await async_client_context.port + await async_client_context.create_user("admin", "admin", "pass") + + await async_client_context.create_user( "pymongo_test", "user", "pass", roles=["userAdmin", "readWrite"] ) @@ -1152,10 +1144,9 @@ async def test_auth_from_uri(self, async_client_context_fixture, async_rs_or_sin await bad_client.pymongo_test.test.find_one() @pytest.mark.usefixtures("require_auth") - async def test_username_and_password(self, async_client_context_fixture, async_rs_or_single_client_noauth): - await async_client_context_fixture.create_user("admin", "ad min", "pa/ss") - # TODO - # self.addAsyncCleanup(async_client_context.drop_user, "admin", "ad min") + @pytest.mark.parametrize("drop_user_fixture", [("admin", "ad min")], indirect=True) + async def test_username_and_password(self, async_client_context, async_rs_or_single_client_noauth, drop_user_fixture): + await async_client_context.create_user("admin", "ad min", "pa/ss") c = await async_rs_or_single_client_noauth(username="ad min", password="pa/ss") @@ -1175,8 +1166,8 @@ async def test_username_and_password(self, async_client_context_fixture, async_r @pytest.mark.usefixtures("require_auth") @pytest.mark.usefixtures("require_no_fips") - async def test_lazy_auth_raises_operation_failure(self, async_client_context_fixture, async_rs_or_single_client_noauth): - host = await async_client_context_fixture.host + async def test_lazy_auth_raises_operation_failure(self, async_client_context, async_rs_or_single_client_noauth): + host = await async_client_context.host lazy_client = await async_rs_or_single_client_noauth( f"mongodb://user:wrong@{host}/pymongo_test", connect=False ) @@ -1185,12 +1176,12 @@ async def test_lazy_auth_raises_operation_failure(self, async_client_context_fix @pytest.mark.usefixtures("require_no_tls") - async def test_unix_socket(self, async_client_context_fixture, async_rs_or_single_client, simple_client): + async def test_unix_socket(self, async_client_context, async_rs_or_single_client, simple_client): if not hasattr(socket, "AF_UNIX"): pytest.skip("UNIX-sockets are not supported on this system") - mongodb_socket = "/tmp/mongodb-%d.sock" % (await async_client_context_fixture.port,) - encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (await async_client_context_fixture.port,) + mongodb_socket = "/tmp/mongodb-%d.sock" % (await async_client_context.port,) + encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (await async_client_context.port,) if not os.access(mongodb_socket, os.R_OK): pytest.skip("Socket file is not accessible") @@ -1210,8 +1201,8 @@ async def test_unix_socket(self, async_client_context_fixture, async_rs_or_singl ) await connected(c) - async def test_document_class(self, async_client, async_rs_or_single_client): - c = async_client + async def test_document_class(self, async_client_context, async_rs_or_single_client): + c = async_client_context.client db = c.pymongo_test await db.test.insert_one({"x": 1}) @@ -1262,8 +1253,8 @@ async def test_socket_timeout_ms_validation(self, async_rs_or_single_client): async with await async_rs_or_single_client(socketTimeoutMS="foo"): pass - async def test_socket_timeout(self, async_client, async_rs_or_single_client): - no_timeout = async_client + async def test_socket_timeout(self, async_client_context, async_rs_or_single_client): + no_timeout = async_client_context.client timeout_sec = 1 timeout = await async_rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) @@ -1321,18 +1312,18 @@ async def test_waitQueueTimeoutMS(self, async_rs_or_single_client): client = await async_rs_or_single_client(waitQueueTimeoutMS=2000) assert 2 == (await async_get_pool(client)).opts.wait_queue_timeout - async def test_socketKeepAlive(self, async_client): - pool = await async_get_pool(async_client) + async def test_socketKeepAlive(self, async_client_context): + pool = await async_get_pool(async_client_context.client) async with pool.checkout() as conn: keepalive = conn.conn.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) assert keepalive @no_type_check - async def test_tz_aware(self, async_client, async_rs_or_single_client): + async def test_tz_aware(self, async_client_context, async_rs_or_single_client): pytest.raises(ValueError, AsyncMongoClient, tz_aware="foo") aware = await async_rs_or_single_client(tz_aware=True) - naive = async_client + naive = async_client_context.client await aware.pymongo_test.drop_collection("test") now = datetime.datetime.now(tz=datetime.timezone.utc) @@ -1346,19 +1337,19 @@ async def test_tz_aware(self, async_client, async_rs_or_single_client): ) @pytest.mark.usefixtures("require_ipv6") - async def test_ipv6(self, async_client_context_fixture, async_rs_or_single_client_noauth): - if async_client_context_fixture.tls: + async def test_ipv6(self, async_client_context, async_rs_or_single_client_noauth): + if async_client_context.tls: if not HAVE_IPADDRESS: pytest.skip("Need the ipaddress module to test with SSL") - if async_client_context_fixture.auth_enabled: + if async_client_context.auth_enabled: auth_str = f"{db_user}:{db_pwd}@" else: auth_str = "" - uri = "mongodb://%s[::1]:%d" % (auth_str, await async_client_context_fixture.port) - if async_client_context_fixture.is_rs: - uri += "/?replicaSet=" + (async_client_context_fixture.replica_set_name or "") + uri = "mongodb://%s[::1]:%d" % (auth_str, await async_client_context.port) + if async_client_context.is_rs: + uri += "/?replicaSet=" + (async_client_context.replica_set_name or "") client = await async_rs_or_single_client_noauth(uri) await client.pymongo_test.test.insert_one({"dummy": "object"}) @@ -1390,7 +1381,7 @@ async def test_contextlib(self, async_rs_or_single_client): await client.pymongo_test.test.find_one() @pytest.mark.usefixtures("require_sync") - def test_interrupt_signal(self, async_client): + def test_interrupt_signal(self, async_client_context): if sys.platform.startswith("java"): # We can't figure out how to raise an exception on a thread that's # blocked on a socket, whether that's the main thread or a worker, @@ -1402,7 +1393,7 @@ def test_interrupt_signal(self, async_client): # Test fix for PYTHON-294 -- make sure AsyncMongoClient closes its # socket if it gets an interrupt while waiting to recv() from it. - db = async_client.pymongo_test + db = async_client_context.client.pymongo_test # A $where clause which takes 1.5 sec to execute where = delay(1.5) @@ -1474,15 +1465,14 @@ async def test_operation_failure(self, async_single_client): new_conn = next(iter(pool.conns)) assert old_conn == new_conn - async def test_lazy_connect_w0(self, async_client_context_fixture, async_rs_or_single_client): + @pytest.mark.parametrize("drop_database_fixture", ["test_lazy_connect_w0"], indirect=True) + async def test_lazy_connect_w0(self, async_client_context, async_rs_or_single_client, drop_database_fixture): # Ensure that connect-on-demand works when the first operation is # an unacknowledged write. This exercises _writable_max_wire_version(). # Use a separate collection to avoid races where we're still # completing an operation on a collection while the next test begins. - await async_client_context_fixture.client.drop_database("test_lazy_connect_w0") - # TODO - # self.addAsyncCleanup(async_client_context.client.drop_database, "test_lazy_connect_w0") + await async_client_context.client.drop_database("test_lazy_connect_w0") client = await async_rs_or_single_client(connect=False, w=0) await client.test_lazy_connect_w0.test.insert_one({}) @@ -1590,7 +1580,7 @@ async def test_stale_getmore(self, async_rs_client): address=("not-a-member", 27017), ) - async def test_heartbeat_frequency_ms(self, async_client_context_fixture, async_single_client): + async def test_heartbeat_frequency_ms(self, async_client_context, async_single_client): class HeartbeatStartedListener(ServerHeartbeatListener): def __init__(self): self.results = [] @@ -1615,8 +1605,8 @@ def init(self, *args): ServerHeartbeatStartedEvent.__init__ = init # type: ignore listener = HeartbeatStartedListener() uri = "mongodb://%s:%d/?heartbeatFrequencyMS=500" % ( - await async_client_context_fixture.host, - await async_client_context_fixture.port, + await async_client_context.host, + await async_client_context.port, ) await async_single_client(uri, event_listeners=[listener]) await async_wait_until( @@ -1638,7 +1628,7 @@ async def test_small_heartbeat_frequency_ms(self): assert "heartbeatFrequencyMS" in str(context.value) - async def test_compression(self, async_client_context_fixture, simple_client, async_single_client): + async def test_compression(self, async_client_context, simple_client, async_single_client): def compression_settings(client): pool_options = client.options.pool_options return pool_options._compression_settings @@ -1706,7 +1696,7 @@ def compression_settings(client): opts = compression_settings(client) assert opts.compressors == ["zstd", "zlib"] - options = async_client_context_fixture.default_client_options + options = async_client_context.default_client_options if "compressors" in options and "zlib" in options["compressors"]: for level in range(-1, 10): client = await async_single_client(zlibcompressionlevel=level) @@ -1768,18 +1758,18 @@ async def stall_connect(*args, **kwargs): await asyncio.sleep(2) return await original_connect(*args, **kwargs) - pool.connect = stall_connect - # TODO - # self.addCleanup(delattr, pool, "connect") - - await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") - - # Assert that application operations do not block. - for _ in range(10): - start = time.monotonic() - await client.admin.command("ping") - total = time.monotonic() - start - assert total < 2 + try: + pool.connect = stall_connect + + await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") + # Assert that application operations do not block. + for _ in range(10): + start = time.monotonic() + await client.admin.command("ping") + total = time.monotonic() - start + assert total < 2 + finally: + delattr(pool, "connect") @pytest.mark.usefixtures("require_replica_set") async def test_direct_connection(self, async_rs_or_single_client): @@ -1876,28 +1866,25 @@ async def test_srv_max_hosts_kwarg(self, simple_client): def test_sigstop_sigcont(self, async_client_context): test_dir = os.path.dirname(os.path.realpath(__file__)) script = os.path.join(test_dir, "sigstop_sigcont.py") - p = subprocess.Popen( + with subprocess.Popen( [sys.executable, script, async_client_context.uri], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - # TODO - # self.addCleanup(p.wait, timeout=1) - # self.addCleanup(p.kill) - time.sleep(1) - os.kill(p.pid, signal.SIGSTOP) - time.sleep(2) - os.kill(p.pid, signal.SIGCONT) - time.sleep(0.5) - outs, _ = p.communicate(input=b"q\n", timeout=10) - assert outs - log_output = outs.decode("utf-8") - assert "TEST STARTED" in log_output - assert "ServerHeartbeatStartedEvent" in log_output - assert "ServerHeartbeatSucceededEvent" in log_output - assert "TEST COMPLETED" in log_output - assert "ServerHeartbeatFailedEvent" not in log_output + ) as p: + time.sleep(1) + os.kill(p.pid, signal.SIGSTOP) + time.sleep(2) + os.kill(p.pid, signal.SIGCONT) + time.sleep(0.5) + outs, _ = p.communicate(input=b"q\n", timeout=10) + assert outs + log_output = outs.decode("utf-8") + assert "TEST STARTED" in log_output + assert "ServerHeartbeatStartedEvent" in log_output + assert "ServerHeartbeatSucceededEvent" in log_output + assert "TEST COMPLETED" in log_output + assert "ServerHeartbeatFailedEvent" not in log_output async def _test_handshake(self, env_vars, expected_env, async_rs_or_single_client): with patch.dict("os.environ", env_vars): @@ -2012,23 +1999,23 @@ async def test_handshake_09_container_with_provider(self, async_rs_or_single_cli async_rs_or_single_client, ) - async def test_dict_hints(self, async_client): - async_client.db.t.find(hint={"x": 1}) + async def test_dict_hints(self, async_client_context): + async_client_context.client.db.t.find(hint={"x": 1}) - async def test_dict_hints_sort(self, async_client): - result = async_client.db.t.find() + async def test_dict_hints_sort(self, async_client_context): + result = async_client_context.client.db.t.find() result.sort({"x": 1}) - async_client.db.t.find(sort={"x": 1}) + async_client_context.client.db.t.find(sort={"x": 1}) - async def test_dict_hints_create_index(self, async_client): - await async_client.db.t.create_index({"x": pymongo.ASCENDING}) + async def test_dict_hints_create_index(self, async_client_context): + await async_client_context.client.db.t.create_index({"x": pymongo.ASCENDING}) - async def test_legacy_java_uuid_roundtrip(self, async_client_context_fixture): + async def test_legacy_java_uuid_roundtrip(self, async_client_context): data = BinaryData.java_data docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, JAVA_LEGACY)) - await async_client_context_fixture.client.pymongo_test.drop_collection("java_uuid") - db = async_client_context_fixture.client.pymongo_test + await async_client_context.client.pymongo_test.drop_collection("java_uuid") + db = async_client_context.client.pymongo_test coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=JAVA_LEGACY)) await coll.insert_many(docs) @@ -2039,14 +2026,14 @@ async def test_legacy_java_uuid_roundtrip(self, async_client_context_fixture): coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) async for d in coll.find(): assert d["newguid"] != d["newguidstring"] - await async_client_context_fixture.client.pymongo_test.drop_collection("java_uuid") + await async_client_context.client.pymongo_test.drop_collection("java_uuid") - async def test_legacy_csharp_uuid_roundtrip(self, async_client_context_fixture): + async def test_legacy_csharp_uuid_roundtrip(self, async_client_context): data = BinaryData.csharp_data docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, CSHARP_LEGACY)) - await async_client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") - db = async_client_context_fixture.client.pymongo_test + await async_client_context.client.pymongo_test.drop_collection("csharp_uuid") + db = async_client_context.client.pymongo_test coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=CSHARP_LEGACY)) await coll.insert_many(docs) @@ -2057,15 +2044,15 @@ async def test_legacy_csharp_uuid_roundtrip(self, async_client_context_fixture): coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) async for d in coll.find(): assert d["newguid"] != d["newguidstring"] - await async_client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") + await async_client_context.client.pymongo_test.drop_collection("csharp_uuid") async def test_uri_to_uuid(self, async_single_client): uri = "mongodb://foo/?uuidrepresentation=csharpLegacy" client = await async_single_client(uri, connect=False) assert client.pymongo_test.test.codec_options.uuid_representation == CSHARP_LEGACY - async def test_uuid_queries(self, async_client_context_fixture): - db = async_client_context_fixture.client.pymongo_test + async def test_uuid_queries(self, async_client_context): + db = async_client_context.client.pymongo_test coll = db.test await coll.drop() @@ -2089,6 +2076,7 @@ async def test_uuid_queries(self, async_client_context_fixture): await coll.drop() @pytest.mark.usefixtures("require_no_mongos") +@pytest.mark.usefixtures("integration_test") class TestAsyncExhaustCursor(AsyncPyMongoTestCasePyTest): async def test_exhaust_query_server_error(self, async_rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success @@ -2207,14 +2195,14 @@ async def test_exhaust_getmore_network_error(self, async_rs_or_single_client): assert 0 == pool.requests @pytest.mark.usefixtures("require_sync") - def test_gevent_task(self, async_client_context_fixture): + def test_gevent_task(self, async_client_context): if not gevent_monkey_patched(): pytest.skip("Must be running monkey patched by gevent") from gevent import spawn def poller(): while True: - async_client_context_fixture.client.pymongo_test.test.insert_one({}) + async_client_context.client.pymongo_test.test.insert_one({}) task = spawn(poller) task.kill() @@ -2291,6 +2279,7 @@ def timeout_task(): del pool.connect @pytest.mark.usefixtures("require_sync") +@pytest.mark.usefixtures("integration_test") class TestClientLazyConnect: """Test concurrent operations on a lazily-connecting MongoClient.""" @@ -2352,7 +2341,7 @@ def test(collection): lazy_client_trial(reset, find_one, test, self._get_client) -class TestMongoClientFailover(): +class TestMongoClientFailover: async def test_discover_primary(self, async_mock_client): c = await async_mock_client( standalones=[], @@ -2477,8 +2466,7 @@ async def callback(client): await self._test_network_error(async_mock_client, callback) -# TODO: replace require_connection -# @pytest.mark.usefixtures("require_connection") +@pytest.mark.usefixtures("integration_test") class TestAsyncClientPool: async def test_rs_client_does_not_maintain_pool_to_arbiters(self, async_mock_client): listener = CMAPListener() @@ -2525,4 +2513,3 @@ async def test_direct_client_maintains_pool_to_arbiter(self, async_mock_client): arbiter = c._topology.get_server_by_address(("c", 3)) assert len(arbiter.pool.conns) == 1 assert listener.event_count(monitoring.PoolReadyEvent) == 1 - From 84211e7c7c952df95c41e0fda946a244406e13a3 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 13:06:38 -0500 Subject: [PATCH 10/29] Lots of cleanup --- justfile | 4 + pymongo/asynchronous/mongo_client.py | 3 +- pymongo/synchronous/mongo_client.py | 2 + pyproject.toml | 4 +- test/__init__.py | 31 +- test/asynchronous/__init__.py | 22 +- test/asynchronous/conftest.py | 239 ++- test/asynchronous/pymongo_mocks.py | 3 +- test/asynchronous/test_client.py | 2125 +++++++++---------- test/asynchronous/test_client_pytest.py | 2515 ----------------------- test/conftest.py | 344 +++- test/pymongo_mocks.py | 3 +- test/test_client.py | 2044 +++++++++--------- test/test_custom_types.py | 3 +- test/utils.py | 14 +- tools/synchro.py | 23 +- uv.lock | 617 +----- 17 files changed, 2707 insertions(+), 5289 deletions(-) delete mode 100644 test/asynchronous/test_client_pytest.py diff --git a/justfile b/justfile index 6bcfe0c79c..d822ab25df 100644 --- a/justfile +++ b/justfile @@ -62,6 +62,10 @@ lint-manual: test *args="-v --durations=5 --maxfail=10": {{uv_run}} --extra test pytest {{args}} +[group('test')] +test-async *args="-v --durations=5 --maxfail=10 -m asyncio": + {{uv_run}} --extra test pytest {{args}} + [group('test')] test-mockupdb *args: {{uv_run}} -v --extra test --group mockupdb pytest -m mockupdb {{args}} diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 6a83de4cc1..bf44a776ca 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1418,7 +1418,8 @@ def __next__(self) -> NoReturn: raise TypeError("'AsyncMongoClient' object is not iterable") next = __next__ - anext = next + if not _IS_SYNC: + anext = next async def _server_property(self, attr_name: str) -> Any: """An attribute of the current server's description. diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 1b8f9dc5f7..65c920f3d5 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -1414,6 +1414,8 @@ def __next__(self) -> NoReturn: raise TypeError("'MongoClient' object is not iterable") next = __next__ + if not _IS_SYNC: + next = next def _server_property(self, attr_name: str) -> Any: """An attribute of the current server's description. diff --git a/pyproject.toml b/pyproject.toml index d25bed8dd9..75e130594e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ zstd = ["requirements/zstd.txt"] [tool.pytest.ini_options] minversion = "7" -addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async or asyncio"] +addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async"] testpaths = ["test"] log_cli_level = "INFO" faulthandler_timeout = 1500 @@ -135,6 +135,8 @@ markers = [ "mockupdb: tests that rely on mockupdb", "default: default test suite", "default_async: default async test suite", + "unit: tests that don't require a connection to MongoDB", + "integration: tests that require a connection to MongoDB", ] [tool.mypy] diff --git a/test/__init__.py b/test/__init__.py index ed7966f718..d6aaf123c2 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -15,18 +15,14 @@ """Synchronous test suite for pymongo, bson, and gridfs.""" from __future__ import annotations -import asyncio import gc -import logging import multiprocessing import os import signal import socket import subprocess import sys -import threading import time -import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -53,6 +49,7 @@ sanitize_reply, ) +from pymongo.lock import _create_lock from pymongo.uri_parser import parse_uri try: @@ -116,7 +113,7 @@ def __init__(self): self.default_client_options: Dict = {} self.sessions_enabled = False self.client = None # type: ignore - self.conn_lock = threading.Lock() + self.conn_lock = _create_lock() self.is_data_lake = False self.load_balancer = TEST_LOADBALANCER self.serverless = TEST_SERVERLESS @@ -518,6 +515,12 @@ def require_data_lake(self, func): func=func, ) + @property + def is_not_mmap(self): + if self.is_mongos: + return True + return self.storage_engine != "mmapv1" + def require_no_mmap(self, func): """Run a test only if the server is not using the MMAPv1 storage engine. Only works for standalone and replica sets; tests are @@ -571,6 +574,10 @@ def require_replica_set(self, func): """Run a test only if the client is connected to a replica set.""" return self._require(lambda: self.is_rs, "Not connected to a replica set", func=func) + @property + def secondaries_count(self): + return 0 if not self.client else len(self.client.secondaries) + def require_secondaries_count(self, count): """Run a test only if the client is connected to a replica set that has `count` secondaries. @@ -690,7 +697,7 @@ def is_topology_type(self, topologies): if "sharded" in topologies and self.is_mongos: return True if "sharded-replicaset" in topologies and self.is_mongos: - shards = client_context.client.config.shards.find().to_list() + shards = self.client.config.shards.find().to_list() for shard in shards: # For a 3-member RS-backed sharded cluster, shard['host'] # will be 'replicaName/ip1:port1,ip2:port2,ip3:port3' @@ -864,6 +871,18 @@ def max_message_size_bytes(self): client_context = ClientContext() +class PyMongoTestCasePyTest: + @contextmanager + def fail_point(self, client, command_args): + cmd_on = SON([("configureFailPoint", "failCommand")]) + cmd_on.update(command_args) + client.admin.command(cmd_on) + try: + yield + finally: + client.admin.command("configureFailPoint", cmd_on["configureFailPoint"], mode="off") + + class PyMongoTestCase(unittest.TestCase): def assertEqualCommand(self, expected, actual, msg=None): self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 28fa6f434e..616137cfe9 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -872,6 +872,7 @@ async def max_message_size_bytes(self): # Reusable client context async_client_context = AsyncClientContext() + class AsyncPyMongoTestCasePyTest: @asynccontextmanager async def fail_point(self, client, command_args): @@ -1212,6 +1213,7 @@ async def asyncTearDown(self) -> None: async def async_setup(): + await async_client_context.init() warnings.resetwarnings() warnings.simplefilter("always") global_knobs.enable() @@ -1226,16 +1228,16 @@ async def async_teardown(): garbage.append(f" gc.get_referrers: {gc.get_referrers(g)!r}") if garbage: raise AssertionError("\n".join(garbage)) - # c = async_client_context.client - # if c: - # if not async_client_context.is_data_lake: - # await c.drop_database("pymongo-pooling-tests") - # await c.drop_database("pymongo_test") - # await c.drop_database("pymongo_test1") - # await c.drop_database("pymongo_test2") - # await c.drop_database("pymongo_test_mike") - # await c.drop_database("pymongo_test_bernie") - # await c.close() + c = async_client_context.client + if c: + if not async_client_context.is_data_lake: + await c.drop_database("pymongo-pooling-tests") + await c.drop_database("pymongo_test") + await c.drop_database("pymongo_test1") + await c.drop_database("pymongo_test2") + await c.drop_database("pymongo_test_mike") + await c.drop_database("pymongo_test_bernie") + await c.close() print_running_clients() diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index f123f8d37f..edb25604c5 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -8,19 +8,16 @@ _connection_string, async_setup, async_teardown, - remove_all_users, ) from test.asynchronous.pymongo_mocks import AsyncMockClient from test.utils import FunctionCallRecorder -from typing import Callable +from typing import Any, Callable import pytest import pytest_asyncio -from typing_extensions import Any import pymongo from pymongo import AsyncMongoClient -from pymongo.asynchronous.database import AsyncDatabase from pymongo.uri_parser import parse_uri _IS_SYNC = False @@ -36,46 +33,61 @@ def event_loop_policy(): return asyncio.get_event_loop_policy() + @pytest_asyncio.fixture(loop_scope="session", scope="session") -async def async_client_context(): +async def async_client_context_fixture(): client = AsyncClientContext() await client.init() yield client if client.client is not None: + if not client.is_data_lake: + await client.client.drop_database("pymongo-pooling-tests") + await client.client.drop_database("pymongo_test") + await client.client.drop_database("pymongo_test1") + await client.client.drop_database("pymongo_test2") + await client.client.drop_database("pymongo_test_mike") + await client.client.drop_database("pymongo_test_bernie") await client.client.close() @pytest_asyncio.fixture -async def integration_test(async_client_context): - if not async_client_context.connected: +async def require_integration(async_client_context_fixture): + if not async_client_context_fixture.connected: pytest.fail("Integration tests require a MongoDB server") + @pytest_asyncio.fixture(loop_scope="session", scope="session") -async def test_environment(async_client_context): +async def test_environment(async_client_context_fixture): requirements = {} - requirements["SUPPORT_TRANSACTIONS"] = async_client_context.supports_transactions() - requirements["IS_DATA_LAKE"] = async_client_context.is_data_lake + requirements["SUPPORT_TRANSACTIONS"] = async_client_context_fixture.supports_transactions() + requirements["IS_DATA_LAKE"] = async_client_context_fixture.is_data_lake requirements["IS_SYNC"] = _IS_SYNC requirements["IS_SYNC"] = _IS_SYNC requirements["REQUIRE_API_VERSION"] = MONGODB_API_VERSION - requirements["SUPPORTS_FAILCOMMAND_FAIL_POINT"] = async_client_context.supports_failCommand_fail_point - requirements["IS_NOT_MMAP"] = async_client_context.is_not_mmap - requirements["SERVER_VERSION"] = async_client_context.version - requirements["AUTH_ENABLED"] = async_client_context.auth_enabled - requirements["FIPS_ENABLED"] = async_client_context.fips_enabled - requirements["IS_RS"] = async_client_context.is_rs - requirements["MONGOSES"] = len(async_client_context.mongoses) - requirements["SECONDARIES_COUNT"] = await async_client_context.secondaries_count - requirements["SECONDARY_READ_PREF"] = await async_client_context.supports_secondary_read_pref - requirements["HAS_IPV6"] = async_client_context.has_ipv6 - requirements["IS_SERVERLESS"] = async_client_context.serverless - requirements["IS_LOAD_BALANCER"] = async_client_context.load_balancer - requirements["TEST_COMMANDS_ENABLED"] = async_client_context.test_commands_enabled - requirements["IS_TLS"] = async_client_context.tls - requirements["IS_TLS_CERT"] = async_client_context.tlsCertificateKeyFile - requirements["SERVER_IS_RESOLVEABLE"] = async_client_context.server_is_resolvable - requirements["SESSIONS_ENABLED"] = async_client_context.sessions_enabled - requirements["SUPPORTS_RETRYABLE_WRITES"] = async_client_context.supports_retryable_writes() + requirements[ + "SUPPORTS_FAILCOMMAND_FAIL_POINT" + ] = async_client_context_fixture.supports_failCommand_fail_point + requirements["IS_NOT_MMAP"] = async_client_context_fixture.is_not_mmap + requirements["SERVER_VERSION"] = async_client_context_fixture.version + requirements["AUTH_ENABLED"] = async_client_context_fixture.auth_enabled + requirements["FIPS_ENABLED"] = async_client_context_fixture.fips_enabled + requirements["IS_RS"] = async_client_context_fixture.is_rs + requirements["MONGOSES"] = len(async_client_context_fixture.mongoses) + requirements["SECONDARIES_COUNT"] = await async_client_context_fixture.secondaries_count + requirements[ + "SECONDARY_READ_PREF" + ] = await async_client_context_fixture.supports_secondary_read_pref + requirements["HAS_IPV6"] = async_client_context_fixture.has_ipv6 + requirements["IS_SERVERLESS"] = async_client_context_fixture.serverless + requirements["IS_LOAD_BALANCER"] = async_client_context_fixture.load_balancer + requirements["TEST_COMMANDS_ENABLED"] = async_client_context_fixture.test_commands_enabled + requirements["IS_TLS"] = async_client_context_fixture.tls + requirements["IS_TLS_CERT"] = async_client_context_fixture.tlsCertificateKeyFile + requirements["SERVER_IS_RESOLVEABLE"] = async_client_context_fixture.server_is_resolvable + requirements["SESSIONS_ENABLED"] = async_client_context_fixture.sessions_enabled + requirements[ + "SUPPORTS_RETRYABLE_WRITES" + ] = async_client_context_fixture.supports_retryable_writes() yield requirements @@ -84,46 +96,61 @@ async def require_auth(test_environment): if not test_environment["AUTH_ENABLED"]: pytest.skip("Authentication is not enabled on the server") + @pytest_asyncio.fixture async def require_no_fips(test_environment): if test_environment["FIPS_ENABLED"]: pytest.skip("Test cannot run on a FIPS-enabled host") + @pytest_asyncio.fixture async def require_no_tls(test_environment): if test_environment["IS_TLS"]: pytest.skip("Must be able to connect without TLS") + @pytest_asyncio.fixture async def require_ipv6(test_environment): if not test_environment["HAS_IPV6"]: pytest.skip("No IPv6") + @pytest_asyncio.fixture async def require_sync(test_environment): if not _IS_SYNC: pytest.skip("This test only works with the synchronous API") + @pytest_asyncio.fixture async def require_no_mongos(test_environment): if test_environment["MONGOSES"]: pytest.skip("Must be connected to a mongod, not a mongos") + @pytest_asyncio.fixture async def require_no_replica_set(test_environment): if test_environment["IS_RS"]: pytest.skip("Connected to a replica set, not a standalone mongod") + @pytest_asyncio.fixture async def require_replica_set(test_environment): if not test_environment["IS_RS"]: pytest.skip("Not connected to a replica set") + @pytest_asyncio.fixture async def require_sdam(test_environment): if test_environment["IS_SERVERLESS"] or test_environment["IS_LOAD_BALANCER"]: pytest.skip("loadBalanced and serverless clients do not run SDAM") + +@pytest_asyncio.fixture +async def require_no_load_balancer(test_environment): + if test_environment["IS_LOAD_BALANCER"]: + pytest.skip("Must not be connected to a load balancer") + + @pytest_asyncio.fixture async def require_failCommand_fail_point(test_environment): if not test_environment["SUPPORTS_FAILCOMMAND_FAIL_POINT"]: @@ -136,114 +163,144 @@ async def test_setup_and_teardown(): yield await async_teardown() + async def _async_mongo_client( - async_client_context, host, port, authenticate=True, directConnection=None, **kwargs - ): - """Create a new client over SSL/TLS if necessary.""" - host = host or await async_client_context.host - port = port or await async_client_context.port - client_options: dict = async_client_context.default_client_options.copy() - if async_client_context.replica_set_name and not directConnection: - client_options["replicaSet"] = async_client_context.replica_set_name - if directConnection is not None: - client_options["directConnection"] = directConnection - client_options.update(kwargs) - - uri = _connection_string(host) - auth_mech = kwargs.get("authMechanism", "") - if async_client_context.auth_enabled and authenticate and auth_mech != "MONGODB-OIDC": - # Only add the default username or password if one is not provided. - res = parse_uri(uri) - if ( - not res["username"] - and not res["password"] - and "username" not in client_options - and "password" not in client_options - ): - client_options["username"] = db_user - client_options["password"] = db_pwd - client = AsyncMongoClient(uri, port, **client_options) - if client._options.connect: - await client.aconnect() - return client + async_client_context_fixture, host, port, authenticate=True, directConnection=None, **kwargs +): + """Create a new client over SSL/TLS if necessary.""" + host = host or await async_client_context_fixture.host + port = port or await async_client_context_fixture.port + client_options: dict = async_client_context_fixture.default_client_options.copy() + if async_client_context_fixture.replica_set_name and not directConnection: + client_options["replicaSet"] = async_client_context_fixture.replica_set_name + if directConnection is not None: + client_options["directConnection"] = directConnection + client_options.update(kwargs) + + uri = _connection_string(host) + auth_mech = kwargs.get("authMechanism", "") + if async_client_context_fixture.auth_enabled and authenticate and auth_mech != "MONGODB-OIDC": + # Only add the default username or password if one is not provided. + res = parse_uri(uri) + if ( + not res["username"] + and not res["password"] + and "username" not in client_options + and "password" not in client_options + ): + client_options["username"] = db_user + client_options["password"] = db_pwd + client = AsyncMongoClient(uri, port, **client_options) + if client._options.connect: + await client.aconnect() + return client @pytest_asyncio.fixture(loop_scope="session") -async def async_single_client_noauth(async_client_context) -> Callable[..., AsyncMongoClient]: +async def async_single_client_noauth( + async_client_context_fixture +) -> Callable[..., AsyncMongoClient]: """Make a direct connection. Don't authenticate.""" clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context, h, p, authenticate=False, directConnection=True, **kwargs) + client = await _async_mongo_client( + async_client_context_fixture, h, p, authenticate=False, directConnection=True, **kwargs + ) clients.append(client) return client + yield _make_client for client in clients: await client.close() + @pytest_asyncio.fixture(loop_scope="session") -async def async_single_client(async_client_context) -> Callable[..., AsyncMongoClient]: +async def async_single_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: """Make a direct connection, and authenticate if necessary.""" clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context, h, p, directConnection=True, **kwargs) + client = await _async_mongo_client( + async_client_context_fixture, h, p, directConnection=True, **kwargs + ) clients.append(client) return client + yield _make_client for client in clients: await client.close() + @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_client_noauth(async_client_context) -> Callable[..., AsyncMongoClient]: +async def async_rs_client_noauth(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: """Connect to the replica set. Don't authenticate.""" clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) + client = await _async_mongo_client( + async_client_context_fixture, h, p, authenticate=False, **kwargs + ) clients.append(client) return client + yield _make_client for client in clients: await client.close() @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_client(async_client_context) -> Callable[..., AsyncMongoClient]: +async def async_rs_client(async_client_context_fixture) -> Callable[..., AsyncMongoClient]: """Connect to the replica set and authenticate if necessary.""" clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context, h, p, **kwargs) + client = await _async_mongo_client(async_client_context_fixture, h, p, **kwargs) clients.append(client) return client + yield _make_client for client in clients: await client.close() @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_or_single_client_noauth(async_client_context) -> Callable[..., AsyncMongoClient]: +async def async_rs_or_single_client_noauth( + async_client_context_fixture +) -> Callable[..., AsyncMongoClient]: """Connect to the replica set if there is one, otherwise the standalone. Like rs_or_single_client, but does not authenticate. """ clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context, h, p, authenticate=False, **kwargs) + client = await _async_mongo_client( + async_client_context_fixture, h, p, authenticate=False, **kwargs + ) clients.append(client) return client + yield _make_client for client in clients: await client.close() + @pytest_asyncio.fixture(loop_scope="session") -async def async_rs_or_single_client(async_client_context) -> Callable[..., AsyncMongoClient]: +async def async_rs_or_single_client( + async_client_context_fixture +) -> Callable[..., AsyncMongoClient]: """Connect to the replica set if there is one, otherwise the standalone. Authenticates if necessary. """ clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): - client = await _async_mongo_client(async_client_context, h, p, **kwargs) + client = await _async_mongo_client(async_client_context_fixture, h, p, **kwargs) clients.append(client) return client + yield _make_client for client in clients: await client.close() @@ -252,6 +309,7 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): @pytest_asyncio.fixture(loop_scope="session") async def simple_client() -> Callable[..., AsyncMongoClient]: clients = [] + async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): if not h and not p: client = AsyncMongoClient(**kwargs) @@ -259,10 +317,12 @@ async def _make_client(h: Any = None, p: Any = None, **kwargs: Any): client = AsyncMongoClient(h, p, **kwargs) clients.append(client) return client + yield _make_client for client in clients: await client.close() + @pytest.fixture(scope="function") def patch_resolver(): from pymongo.srv_resolver import _resolve @@ -272,42 +332,53 @@ def patch_resolver(): yield patched_resolver pymongo.srv_resolver._resolve = _resolve + @pytest_asyncio.fixture(loop_scope="session") async def async_mock_client(): - clients = [] + clients = [] - async def _make_client(standalones, + async def _make_client( + standalones, members, mongoses, hello_hosts=None, arbiters=None, down_hosts=None, *args, - **kwargs): - client = await AsyncMockClient.get_async_mock_client(standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs) - clients.append(client) - return client - yield _make_client - for client in clients: - await client.close() + **kwargs, + ): + client = await AsyncMockClient.get_async_mock_client( + standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs + ) + clients.append(client) + return client + + yield _make_client + for client in clients: + await client.close() + @pytest_asyncio.fixture(loop_scope="session") -async def remove_all_users_fixture(async_client_context, request): +async def remove_all_users_fixture(async_client_context_fixture, request): db_name = request.param yield - await async_client_context.client[db_name].command("dropAllUsersFromDatabase", 1, writeConcern={"w": async_client_context.w}) + await async_client_context_fixture.client[db_name].command( + "dropAllUsersFromDatabase", 1, writeConcern={"w": async_client_context_fixture.w} + ) + @pytest_asyncio.fixture(loop_scope="session") -async def drop_user_fixture(async_client_context, request): +async def drop_user_fixture(async_client_context_fixture, request): db, user = request.param yield - await async_client_context.drop_user(db, user) + await async_client_context_fixture.drop_user(db, user) + @pytest_asyncio.fixture(loop_scope="session") -async def drop_database_fixture(async_client_context, request): +async def drop_database_fixture(async_client_context_fixture, request): db = request.param yield - await async_client_context.client.drop_database(db) + await async_client_context_fixture.client.drop_database(db) pytest_collection_modifyitems = pytest_conf.pytest_collection_modifyitems diff --git a/test/asynchronous/pymongo_mocks.py b/test/asynchronous/pymongo_mocks.py index ed2395bc98..4703d3a194 100644 --- a/test/asynchronous/pymongo_mocks.py +++ b/test/asynchronous/pymongo_mocks.py @@ -166,7 +166,8 @@ async def get_async_mock_client( standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs ) - await c.aconnect() + if "connect" not in kwargs or "connect" in kwargs and kwargs["connect"]: + await c.aconnect() return c def kill_host(self, host): diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 744a170be2..717a28712e 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -17,7 +17,6 @@ import _thread as thread import asyncio -import base64 import contextlib import copy import datetime @@ -47,24 +46,18 @@ from test.asynchronous import ( HAVE_IPADDRESS, - AsyncIntegrationTest, - AsyncMockClientTest, - AsyncUnitTest, + AsyncPyMongoTestCasePyTest, SkipTest, - async_client_context, client_knobs, connected, db_pwd, db_user, - remove_all_users, - unittest, ) -from test.asynchronous.pymongo_mocks import AsyncMockClient from test.test_binary import BinaryData from test.utils import ( NTHREADS, CMAPListener, - FunctionCallRecorder, + _default_pytest_mark, async_get_pool, async_wait_until, asyncAssertRaisesExactly, @@ -125,22 +118,19 @@ _IS_SYNC = False -class AsyncClientUnitTest(AsyncUnitTest): - """AsyncMongoClient tests that don't require a server.""" +pytestmark = _default_pytest_mark(_IS_SYNC) - client: AsyncMongoClient - async def asyncSetUp(self) -> None: - self.client = await self.async_rs_or_single_client( - connect=False, serverSelectionTimeoutMS=100 - ) - - @pytest.fixture(autouse=True) - def inject_fixtures(self, caplog): - self._caplog = caplog +@pytest.mark.unit +class TestClientUnitTest: + @pytest_asyncio.fixture(loop_scope="session") + async def async_client(self, async_rs_or_single_client) -> AsyncMongoClient: + client = await async_rs_or_single_client(connect=False, serverSelectionTimeoutMS=100) + yield client + await client.close() - async def test_keyword_arg_defaults(self): - client = self.simple_client( + async def test_keyword_arg_defaults(self, simple_client): + client = await simple_client( socketTimeoutMS=None, connectTimeoutMS=20000, waitQueueTimeoutMS=None, @@ -156,220 +146,269 @@ async def test_keyword_arg_defaults(self): options = client.options pool_opts = options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) + assert pool_opts.socket_timeout is None # socket.Socket.settimeout takes a float in seconds - self.assertEqual(20.0, pool_opts.connect_timeout) - self.assertEqual(None, pool_opts.wait_queue_timeout) - self.assertEqual(None, pool_opts._ssl_context) - self.assertEqual(None, options.replica_set_name) - self.assertEqual(ReadPreference.PRIMARY, client.read_preference) - self.assertAlmostEqual(12, client.options.server_selection_timeout) - - async def test_connect_timeout(self): - client = self.simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) + assert 20.0 == pool_opts.connect_timeout + assert pool_opts.wait_queue_timeout is None + assert pool_opts._ssl_context is None + assert options.replica_set_name is None + assert client.read_preference == ReadPreference.PRIMARY + assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 + + async def test_connect_timeout(self, simple_client): + client = await simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) pool_opts = client.options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) - self.assertEqual(None, pool_opts.connect_timeout) + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None - client = self.simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) + client = await simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) pool_opts = client.options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) - self.assertEqual(None, pool_opts.connect_timeout) + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None - client = self.simple_client( + client = await simple_client( "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False ) pool_opts = client.options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) - self.assertEqual(None, pool_opts.connect_timeout) - - def test_types(self): - self.assertRaises(TypeError, AsyncMongoClient, 1) - self.assertRaises(TypeError, AsyncMongoClient, 1.14) - self.assertRaises(TypeError, AsyncMongoClient, "localhost", "27017") - self.assertRaises(TypeError, AsyncMongoClient, "localhost", 1.14) - self.assertRaises(TypeError, AsyncMongoClient, "localhost", []) - - self.assertRaises(ConfigurationError, AsyncMongoClient, []) - - async def test_max_pool_size_zero(self): - self.simple_client(maxPoolSize=0) - - def test_uri_detection(self): - self.assertRaises(ConfigurationError, AsyncMongoClient, "/foo") - self.assertRaises(ConfigurationError, AsyncMongoClient, "://") - self.assertRaises(ConfigurationError, AsyncMongoClient, "foo/") - - def test_get_db(self): + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None + + async def test_types(self): + with pytest.raises(TypeError): + AsyncMongoClient(1) + with pytest.raises(TypeError): + AsyncMongoClient(1.14) + with pytest.raises(TypeError): + AsyncMongoClient("localhost", "27017") + with pytest.raises(TypeError): + AsyncMongoClient("localhost", 1.14) + with pytest.raises(TypeError): + AsyncMongoClient("localhost", []) + + with pytest.raises(ConfigurationError): + AsyncMongoClient([]) + + async def test_max_pool_size_zero(self, simple_client): + await simple_client(maxPoolSize=0) + + async def test_uri_detection(self): + with pytest.raises(ConfigurationError): + AsyncMongoClient("/foo") + with pytest.raises(ConfigurationError): + AsyncMongoClient("://") + with pytest.raises(ConfigurationError): + AsyncMongoClient("foo/") + + async def test_get_db(self, async_client): def make_db(base, name): return base[name] - self.assertRaises(InvalidName, make_db, self.client, "") - self.assertRaises(InvalidName, make_db, self.client, "te$t") - self.assertRaises(InvalidName, make_db, self.client, "te.t") - self.assertRaises(InvalidName, make_db, self.client, "te\\t") - self.assertRaises(InvalidName, make_db, self.client, "te/t") - self.assertRaises(InvalidName, make_db, self.client, "te st") - - self.assertTrue(isinstance(self.client.test, AsyncDatabase)) - self.assertEqual(self.client.test, self.client["test"]) - self.assertEqual(self.client.test, AsyncDatabase(self.client, "test")) - - def test_get_database(self): + with pytest.raises(InvalidName): + make_db(async_client, "") + with pytest.raises(InvalidName): + make_db(async_client, "te$t") + with pytest.raises(InvalidName): + make_db(async_client, "te.t") + with pytest.raises(InvalidName): + make_db(async_client, "te\\t") + with pytest.raises(InvalidName): + make_db(async_client, "te/t") + with pytest.raises(InvalidName): + make_db(async_client, "te st") + # Type and equality assertions + assert isinstance(async_client.test, AsyncDatabase) + assert async_client.test == async_client["test"] + assert async_client.test == AsyncDatabase(async_client, "test") + + async def test_get_database(self, async_client): codec_options = CodecOptions(tz_aware=True) write_concern = WriteConcern(w=2, j=True) - db = self.client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) - self.assertEqual("foo", db.name) - self.assertEqual(codec_options, db.codec_options) - self.assertEqual(ReadPreference.SECONDARY, db.read_preference) - self.assertEqual(write_concern, db.write_concern) + db = async_client.get_database( + "foo", codec_options, ReadPreference.SECONDARY, write_concern + ) + assert db.name == "foo" + assert db.codec_options == codec_options + assert db.read_preference == ReadPreference.SECONDARY + assert db.write_concern == write_concern - def test_getattr(self): - self.assertTrue(isinstance(self.client["_does_not_exist"], AsyncDatabase)) + async def test_getattr(self, async_client): + assert isinstance(async_client["_does_not_exist"], AsyncDatabase) - with self.assertRaises(AttributeError) as context: - self.client._does_not_exist + with pytest.raises(AttributeError) as context: + async_client.client._does_not_exist # Message should be: # "AttributeError: AsyncMongoClient has no attribute '_does_not_exist'. To # access the _does_not_exist database, use client['_does_not_exist']". - self.assertIn("has no attribute '_does_not_exist'", str(context.exception)) - - def test_iteration(self): - client = self.client - msg = "'AsyncMongoClient' object is not iterable" - # Iteration fails - with self.assertRaisesRegex(TypeError, msg): - for _ in client: # type: ignore[misc] # error: "None" not callable [misc] + assert "has no attribute '_does_not_exist'" in str(context.value) + + async def test_iteration(self, async_client): + if _IS_SYNC: + msg = "'AsyncMongoClient' object is not iterable" + else: + msg = "'AsyncMongoClient' object is not an async iterator" + + with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + for _ in async_client: break + # Index fails - with self.assertRaises(TypeError): - _ = client[0] - # next fails - with self.assertRaisesRegex(TypeError, "'AsyncMongoClient' object is not iterable"): - _ = next(client) - # .next() fails - with self.assertRaisesRegex(TypeError, "'AsyncMongoClient' object is not iterable"): - _ = client.next() - # Do not implement typing.Iterable. - self.assertNotIsInstance(client, Iterable) - - async def test_get_default_database(self): - c = await self.async_rs_or_single_client( + with pytest.raises(TypeError): + _ = async_client[0] + + # 'next' function fails + with pytest.raises(TypeError, match=msg): + _ = await anext(async_client) + + # 'next()' method fails + with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + _ = await async_client.anext() + + # Do not implement typing.Iterable + assert not isinstance(async_client, Iterable) + + async def test_get_default_database( + self, async_rs_or_single_client, async_client_context_fixture + ): + c = await async_rs_or_single_client( "mongodb://%s:%d/foo" - % (await async_client_context.host, await async_client_context.port), + % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) - self.assertEqual(AsyncDatabase(c, "foo"), c.get_default_database()) + assert AsyncDatabase(c, "foo") == c.get_default_database() # Test that default doesn't override the URI value. - self.assertEqual(AsyncDatabase(c, "foo"), c.get_default_database("bar")) - + assert AsyncDatabase(c, "foo") == c.get_default_database("bar") codec_options = CodecOptions(tz_aware=True) write_concern = WriteConcern(w=2, j=True) db = c.get_default_database(None, codec_options, ReadPreference.SECONDARY, write_concern) - self.assertEqual("foo", db.name) - self.assertEqual(codec_options, db.codec_options) - self.assertEqual(ReadPreference.SECONDARY, db.read_preference) - self.assertEqual(write_concern, db.write_concern) - - c = await self.async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), + assert "foo" == db.name + assert codec_options == db.codec_options + assert ReadPreference.SECONDARY == db.read_preference + assert write_concern == db.write_concern + + c = await async_rs_or_single_client( + "mongodb://%s:%d/" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) - self.assertEqual(AsyncDatabase(c, "foo"), c.get_default_database("foo")) + assert AsyncDatabase(c, "foo") == c.get_default_database("foo") - async def test_get_default_database_error(self): + async def test_get_default_database_error( + self, async_rs_or_single_client, async_client_context_fixture + ): # URI with no database. - c = await self.async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), + c = await async_rs_or_single_client( + "mongodb://%s:%d/" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) - self.assertRaises(ConfigurationError, c.get_default_database) + with pytest.raises(ConfigurationError): + c.get_default_database() - async def test_get_default_database_with_authsource(self): + async def test_get_default_database_with_authsource( + self, async_client_context_fixture, async_rs_or_single_client + ): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context.host, - await async_client_context.port, + await async_client_context_fixture.host, + await async_client_context_fixture.port, ) - c = await self.async_rs_or_single_client(uri, connect=False) - self.assertEqual(AsyncDatabase(c, "foo"), c.get_default_database()) + c = await async_rs_or_single_client(uri, connect=False) + assert AsyncDatabase(c, "foo") == c.get_default_database() - async def test_get_database_default(self): - c = await self.async_rs_or_single_client( + async def test_get_database_default( + self, async_client_context_fixture, async_rs_or_single_client + ): + c = await async_rs_or_single_client( "mongodb://%s:%d/foo" - % (await async_client_context.host, await async_client_context.port), + % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) - self.assertEqual(AsyncDatabase(c, "foo"), c.get_database()) + assert AsyncDatabase(c, "foo") == c.get_database() - async def test_get_database_default_error(self): + async def test_get_database_default_error( + self, async_client_context_fixture, async_rs_or_single_client + ): # URI with no database. - c = await self.async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), + c = await async_rs_or_single_client( + "mongodb://%s:%d/" + % (await async_client_context_fixture.host, await async_client_context_fixture.port), connect=False, ) - self.assertRaises(ConfigurationError, c.get_database) + with pytest.raises(ConfigurationError): + c.get_database() - async def test_get_database_default_with_authsource(self): + async def test_get_database_default_with_authsource( + self, async_client_context_fixture, async_rs_or_single_client + ): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context.host, - await async_client_context.port, + await async_client_context_fixture.host, + await async_client_context_fixture.port, ) - c = await self.async_rs_or_single_client(uri, connect=False) - self.assertEqual(AsyncDatabase(c, "foo"), c.get_database()) + c = await async_rs_or_single_client(uri, connect=False) + assert AsyncDatabase(c, "foo") == c.get_database() - async def test_primary_read_pref_with_tags(self): + async def test_primary_read_pref_with_tags(self, async_single_client): # No tags allowed with "primary". - with self.assertRaises(ConfigurationError): - await self.async_single_client("mongodb://host/?readpreferencetags=dc:east") - - with self.assertRaises(ConfigurationError): - await self.async_single_client( + with pytest.raises(ConfigurationError): + async with await async_single_client("mongodb://host/?readpreferencetags=dc:east"): + pass + with pytest.raises(ConfigurationError): + async with await async_single_client( "mongodb://host/?readpreference=primary&readpreferencetags=dc:east" - ) + ): + pass - async def test_read_preference(self): - c = await self.async_rs_or_single_client( + async def test_read_preference(self, async_client_context_fixture, async_rs_or_single_client): + c = await async_rs_or_single_client( "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode ) - self.assertEqual(c.read_preference, ReadPreference.NEAREST) + assert c.read_preference == ReadPreference.NEAREST - async def test_metadata(self): + async def test_metadata(self, simple_client): metadata = copy.deepcopy(_METADATA) if has_c(): metadata["driver"]["name"] = "PyMongo|c|async" else: metadata["driver"]["name"] = "PyMongo|async" metadata["application"] = {"name": "foobar"} - client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + + client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") options = client.options - self.assertEqual(options.pool_options.metadata, metadata) - client = self.simple_client("foo", 27017, appname="foobar", connect=False) + assert options.pool_options.metadata == metadata + + client = await simple_client("foo", 27017, appname="foobar", connect=False) options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata + # No error - self.simple_client(appname="x" * 128) - with self.assertRaises(ValueError): - self.simple_client(appname="x" * 129) - # Bad "driver" options. - self.assertRaises(TypeError, DriverInfo, "Foo", 1, "a") - self.assertRaises(TypeError, DriverInfo, version="1", platform="a") - self.assertRaises(TypeError, DriverInfo) - with self.assertRaises(TypeError): - self.simple_client(driver=1) - with self.assertRaises(TypeError): - self.simple_client(driver="abc") - with self.assertRaises(TypeError): - self.simple_client(driver=("Foo", "1", "a")) - # Test appending to driver info. + await simple_client(appname="x" * 128) + with pytest.raises(ValueError): + await simple_client(appname="x" * 129) + + # Bad "driver" options. + with pytest.raises(TypeError): + DriverInfo("Foo", 1, "a") + with pytest.raises(TypeError): + DriverInfo(version="1", platform="a") + with pytest.raises(TypeError): + DriverInfo() + with pytest.raises(TypeError): + await simple_client(driver=1) + with pytest.raises(TypeError): + await simple_client(driver="abc") + with pytest.raises(TypeError): + await simple_client(driver=("Foo", "1", "a")) + + # Test appending to driver info. if has_c(): metadata["driver"]["name"] = "PyMongo|c|async|FooDriver" else: metadata["driver"]["name"] = "PyMongo|async|FooDriver" metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) - client = self.simple_client( + + client = await simple_client( "foo", 27017, appname="foobar", @@ -377,9 +416,10 @@ async def test_metadata(self): connect=False, ) options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata + metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) - client = self.simple_client( + client = await simple_client( "foo", 27017, appname="foobar", @@ -387,38 +427,35 @@ async def test_metadata(self): connect=False, ) options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata + # Test truncating driver info metadata. - client = self.simple_client( + client = await simple_client( driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), connect=False, ) options = client.options - self.assertLessEqual( - len(bson.encode(options.pool_options.metadata)), - _MAX_METADATA_SIZE, - ) - client = self.simple_client( + assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE + + client = await simple_client( driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), connect=False, ) options = client.options - self.assertLessEqual( - len(bson.encode(options.pool_options.metadata)), - _MAX_METADATA_SIZE, - ) + assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) - def test_container_metadata(self): + async def test_container_metadata(self, simple_client): metadata = copy.deepcopy(_METADATA) metadata["driver"]["name"] = "PyMongo|async" metadata["env"] = {} metadata["env"]["container"] = {"orchestrator": "kubernetes"} - client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + + client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") options = client.options - self.assertEqual(options.pool_options.metadata["env"], metadata["env"]) + assert options.pool_options.metadata["env"] == metadata["env"] - async def test_kwargs_codec_options(self): + async def test_kwargs_codec_options(self, simple_client): class MyFloatType: def __init__(self, x): self.__x = x @@ -440,7 +477,7 @@ def transform_python(self, value): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" tzinfo = utc - c = self.simple_client( + c = await simple_client( document_class=document_class, type_registry=type_registry, tz_aware=tz_aware, @@ -449,18 +486,16 @@ def transform_python(self, value): tzinfo=tzinfo, connect=False, ) - self.assertEqual(c.codec_options.document_class, document_class) - self.assertEqual(c.codec_options.type_registry, type_registry) - self.assertEqual(c.codec_options.tz_aware, tz_aware) - self.assertEqual( - c.codec_options.uuid_representation, - _UUID_REPRESENTATIONS[uuid_representation_label], + assert c.codec_options.document_class == document_class + assert c.codec_options.type_registry == type_registry + assert c.codec_options.tz_aware == tz_aware + assert ( + c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] ) - self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) - self.assertEqual(c.codec_options.tzinfo, tzinfo) + assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler + assert c.codec_options.tzinfo == tzinfo - async def test_uri_codec_options(self): - # Ensure codec options are passed in correctly + async def test_uri_codec_options(self, async_client_context_fixture, simple_client): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" datetime_conversion = "DATETIME_CLAMP" @@ -469,57 +504,40 @@ async def test_uri_codec_options(self): "%s&unicode_decode_error_handler=%s" "&datetime_conversion=%s" % ( - await async_client_context.host, - await async_client_context.port, + await async_client_context_fixture.host, + await async_client_context_fixture.port, uuid_representation_label, unicode_decode_error_handler, datetime_conversion, ) ) - c = self.simple_client(uri, connect=False) - self.assertEqual(c.codec_options.tz_aware, True) - self.assertEqual( - c.codec_options.uuid_representation, - _UUID_REPRESENTATIONS[uuid_representation_label], - ) - self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) - self.assertEqual( - c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] + c = await simple_client(uri, connect=False) + assert c.codec_options.tz_aware is True + assert ( + c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] ) - + assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler + assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] # Change the passed datetime_conversion to a number and re-assert. uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") - c = self.simple_client(uri, connect=False) - self.assertEqual( - c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] - ) + c = await simple_client(uri, connect=False) + assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] - async def test_uri_option_precedence(self): + async def test_uri_option_precedence(self, simple_client): # Ensure kwarg options override connection string options. uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" - c = self.simple_client( + c = await simple_client( uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" ) clopts = c.options opts = clopts._options + assert opts["tls"] is False + assert clopts.replica_set_name == "newname" + assert clopts.read_preference == ReadPreference.SECONDARY_PREFERRED - self.assertEqual(opts["tls"], False) - self.assertEqual(clopts.replica_set_name, "newname") - self.assertEqual(clopts.read_preference, ReadPreference.SECONDARY_PREFERRED) - - async def test_connection_timeout_ms_propagates_to_DNS_resolver(self): - # Patch the resolver. - from pymongo.srv_resolver import _resolve - - patched_resolver = FunctionCallRecorder(_resolve) - pymongo.srv_resolver._resolve = patched_resolver - - def reset_resolver(): - pymongo.srv_resolver._resolve = _resolve - - self.addCleanup(reset_resolver) - - # Setup. + async def test_connection_timeout_ms_propagates_to_DNS_resolver( + self, patch_resolver, simple_client + ): base_uri = "mongodb+srv://test5.test.build.10gen.cc" connectTimeoutMS = 5000 expected_kw_value = 5.0 @@ -527,10 +545,10 @@ def reset_resolver(): expected_uri_value = 6.0 async def test_scenario(args, kwargs, expected_value): - patched_resolver.reset() - self.simple_client(*args, **kwargs) - for _, kw in patched_resolver.call_list(): - self.assertAlmostEqual(kw["lifetime"], expected_value) + patch_resolver.reset() + await simple_client(*args, **kwargs) + for _, kw in patch_resolver.call_list(): + assert pytest.approx(kw["lifetime"], rel=1e-6) == expected_value # No timeout specified. await test_scenario((base_uri,), {}, CONNECT_TIMEOUT) @@ -545,38 +563,38 @@ async def test_scenario(args, kwargs, expected_value): # Timeout specified in both kwargs and connection string. await test_scenario((uri_with_timeout,), kwarg, expected_kw_value) - async def test_uri_security_options(self): + async def test_uri_security_options(self, simple_client): # Ensure that we don't silently override security-related options. - with self.assertRaises(InvalidURI): - self.simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) + with pytest.raises(InvalidURI): + await simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) # Matching SSL and TLS options should not cause errors. - c = self.simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) - self.assertEqual(c.options._options["tls"], False) + c = await simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) + assert c.options._options["tls"] is False # Conflicting tlsInsecure options should raise an error. - with self.assertRaises(InvalidURI): - self.simple_client( + with pytest.raises(InvalidURI): + await simple_client( "mongodb://localhost/?tlsInsecure=true", connect=False, tlsAllowInvalidHostnames=True, ) # Conflicting legacy tlsInsecure options should also raise an error. - with self.assertRaises(InvalidURI): - self.simple_client( + with pytest.raises(InvalidURI): + await simple_client( "mongodb://localhost/?tlsInsecure=true", connect=False, tlsAllowInvalidCertificates=False, ) # Conflicting kwargs should raise InvalidURI - with self.assertRaises(InvalidURI): - self.simple_client(ssl=True, tls=False) + with pytest.raises(InvalidURI): + await simple_client(ssl=True, tls=False) - async def test_event_listeners(self): - c = self.simple_client(event_listeners=[], connect=False) - self.assertEqual(c.options.event_listeners, []) + async def test_event_listeners(self, simple_client): + c = await simple_client(event_listeners=[], connect=False) + assert c.options.event_listeners == [] listeners = [ event_loggers.CommandLogger(), event_loggers.HeartbeatLogger(), @@ -584,28 +602,30 @@ async def test_event_listeners(self): event_loggers.TopologyLogger(), event_loggers.ConnectionPoolLogger(), ] - c = self.simple_client(event_listeners=listeners, connect=False) - self.assertEqual(c.options.event_listeners, listeners) - - async def test_client_options(self): - c = self.simple_client(connect=False) - self.assertIsInstance(c.options, ClientOptions) - self.assertIsInstance(c.options.pool_options, PoolOptions) - self.assertEqual(c.options.server_selection_timeout, 30) - self.assertEqual(c.options.pool_options.max_idle_time_seconds, None) - self.assertIsInstance(c.options.retry_writes, bool) - self.assertIsInstance(c.options.retry_reads, bool) - - def test_validate_suggestion(self): + c = await simple_client(event_listeners=listeners, connect=False) + assert c.options.event_listeners == listeners + + async def test_client_options(self, simple_client): + c = await simple_client(connect=False) + assert isinstance(c.options, ClientOptions) + assert isinstance(c.options.pool_options, PoolOptions) + assert c.options.server_selection_timeout == 30 + assert c.options.pool_options.max_idle_time_seconds is None + assert isinstance(c.options.retry_writes, bool) + assert isinstance(c.options.retry_reads, bool) + + async def test_validate_suggestion(self): """Validate kwargs in constructor.""" for typo in ["auth", "Auth", "AUTH"]: - expected = f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." + expected = ( + f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, " + f"authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." + ) expected = re.escape(expected) - with self.assertRaisesRegex(ConfigurationError, expected): + with pytest.raises(ConfigurationError, match=expected): AsyncMongoClient(**{typo: "standard"}) # type: ignore[arg-type] - @patch("pymongo.srv_resolver._SrvResolver.get_hosts") - def test_detected_environment_logging(self, mock_get_hosts): + async def test_detected_environment_logging(self, caplog): normal_hosts = [ "normal.host.com", "host.cosmos.azure.com", @@ -616,42 +636,47 @@ def test_detected_environment_logging(self, mock_get_hosts): multi_host = ( "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" ) - with self.assertLogs("pymongo", level="INFO") as cm: - for host in normal_hosts: - AsyncMongoClient(host, connect=False) - for host in srv_hosts: - mock_get_hosts.return_value = [(host, 1)] - AsyncMongoClient(host, connect=False) - AsyncMongoClient(multi_host, connect=False) - logs = [record.getMessage() for record in cm.records if record.name == "pymongo.client"] - self.assertEqual(len(logs), 7) - - @patch("pymongo.srv_resolver._SrvResolver.get_hosts") - async def test_detected_environment_warning(self, mock_get_hosts): - with self._caplog.at_level(logging.WARN): - normal_hosts = [ - "host.cosmos.azure.com", - "host.docdb.amazonaws.com", - "host.docdb-elastic.amazonaws.com", - ] - srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] - multi_host = ( - "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" - ) - for host in normal_hosts: - with self.assertWarns(UserWarning): - self.simple_client(host) - for host in srv_hosts: - mock_get_hosts.return_value = [(host, 1)] - with self.assertWarns(UserWarning): - self.simple_client(host) - with self.assertWarns(UserWarning): - self.simple_client(multi_host) - - -class TestClient(AsyncIntegrationTest): - def test_multiple_uris(self): - with self.assertRaises(ConfigurationError): + with caplog.at_level(logging.INFO, logger="pymongo"): + with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: + for host in normal_hosts: + AsyncMongoClient(host, connect=False) + for host in srv_hosts: + mock_get_hosts.return_value = [(host, 1)] + AsyncMongoClient(host, connect=False) + AsyncMongoClient(multi_host, connect=False) + logs = [ + record.getMessage() + for record in caplog.records + if record.name == "pymongo.client" + ] + assert len(logs) == 7 + + async def test_detected_environment_warning(self, caplog, simple_client): + normal_hosts = [ + "host.cosmos.azure.com", + "host.docdb.amazonaws.com", + "host.docdb-elastic.amazonaws.com", + ] + srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] + multi_host = ( + "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" + ) + with caplog.at_level(logging.WARN, logger="pymongo"): + with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: + with pytest.warns(UserWarning): + for host in normal_hosts: + await simple_client(host) + for host in srv_hosts: + mock_get_hosts.return_value = [(host, 1)] + await simple_client(host) + await simple_client(multi_host) + + +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestClientIntegrationTest(AsyncPyMongoTestCasePyTest): + async def test_multiple_uris(self): + with pytest.raises(ConfigurationError): AsyncMongoClient( host=[ "mongodb+srv://cluster-a.abc12.mongodb.net", @@ -660,22 +685,22 @@ def test_multiple_uris(self): ] ) - async def test_max_idle_time_reaper_default(self): + async def test_max_idle_time_reaper_default(self, async_rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): # Assert reaper doesn't remove connections when maxIdleTimeMS not set - client = await self.async_rs_or_single_client() + client = await async_rs_or_single_client() server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) async with server._pool.checkout() as conn: pass - self.assertEqual(1, len(server._pool.conns)) - self.assertTrue(conn in server._pool.conns) + assert 1 == len(server._pool.conns) + assert conn in server._pool.conns - async def test_max_idle_time_reaper_removes_stale_minPoolSize(self): + async def test_max_idle_time_reaper_removes_stale_minPoolSize(self, async_rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): # Assert reaper removes idle socket and replaces it with a new one - client = await self.async_rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1) + client = await async_rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1) server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) @@ -683,14 +708,16 @@ async def test_max_idle_time_reaper_removes_stale_minPoolSize(self): pass # When the reaper runs at the same time as the get_socket, two # connections could be created and checked into the pool. - self.assertGreaterEqual(len(server._pool.conns), 1) + assert len(server._pool.conns) >= 1 await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") await async_wait_until(lambda: len(server._pool.conns) >= 1, "replace stale socket") - async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self): + async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize( + self, async_rs_or_single_client + ): with client_knobs(kill_cursor_frequency=0.1): # Assert reaper respects maxPoolSize when adding new connections. - client = await self.async_rs_or_single_client( + client = await async_rs_or_single_client( maxIdleTimeMS=500, minPoolSize=1, maxPoolSize=1 ) server = await (await client._get_topology()).select_server( @@ -700,39 +727,39 @@ async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self): pass # When the reaper runs at the same time as the get_socket, # maxPoolSize=1 should prevent two connections from being created. - self.assertEqual(1, len(server._pool.conns)) + assert 1 == len(server._pool.conns) await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") await async_wait_until(lambda: len(server._pool.conns) == 1, "replace stale socket") - async def test_max_idle_time_reaper_removes_stale(self): + async def test_max_idle_time_reaper_removes_stale(self, async_rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): - # Assert reaper has removed idle socket and NOT replaced it - client = await self.async_rs_or_single_client(maxIdleTimeMS=500) + # Assert that the reaper has removed the idle socket and NOT replaced it. + client = await async_rs_or_single_client(maxIdleTimeMS=500) server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) async with server._pool.checkout() as conn_one: pass - # Assert that the pool does not close connections prematurely. + # Assert that the pool does not close connections prematurely await asyncio.sleep(0.300) async with server._pool.checkout() as conn_two: pass - self.assertIs(conn_one, conn_two) + assert conn_one is conn_two await async_wait_until( lambda: len(server._pool.conns) == 0, "stale socket reaped and new one NOT added to the pool", ) - async def test_min_pool_size(self): + async def test_min_pool_size(self, async_rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): - client = await self.async_rs_or_single_client() + client = await async_rs_or_single_client() server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) - self.assertEqual(0, len(server._pool.conns)) + assert len(server._pool.conns) == 0 # Assert that pool started up at minPoolSize - client = await self.async_rs_or_single_client(minPoolSize=10) + client = await async_rs_or_single_client(minPoolSize=10) server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) @@ -740,145 +767,154 @@ async def test_min_pool_size(self): lambda: len(server._pool.conns) == 10, "pool initialized with 10 connections", ) - - # Assert that if a socket is closed, a new one takes its place + # Assert that if a socket is closed, a new one takes its place. async with server._pool.checkout() as conn: conn.close_conn(None) await async_wait_until( lambda: len(server._pool.conns) == 10, "a closed socket gets replaced from the pool", ) - self.assertFalse(conn in server._pool.conns) + assert conn not in server._pool.conns - async def test_max_idle_time_checkout(self): + async def test_max_idle_time_checkout(self, async_rs_or_single_client): # Use high frequency to test _get_socket_no_auth. with client_knobs(kill_cursor_frequency=99999999): - client = await self.async_rs_or_single_client(maxIdleTimeMS=500) + client = await async_rs_or_single_client(maxIdleTimeMS=500) server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) async with server._pool.checkout() as conn: pass - self.assertEqual(1, len(server._pool.conns)) + assert len(server._pool.conns) == 1 await asyncio.sleep(1) # Sleep so that the socket becomes stale. - async with server._pool.checkout() as new_con: - self.assertNotEqual(conn, new_con) - self.assertEqual(1, len(server._pool.conns)) - self.assertFalse(conn in server._pool.conns) - self.assertTrue(new_con in server._pool.conns) + async with server._pool.checkout() as new_conn: + assert conn != new_conn + assert len(server._pool.conns) == 1 + assert conn not in server._pool.conns + assert new_conn in server._pool.conns # Test that connections are reused if maxIdleTimeMS is not set. - client = await self.async_rs_or_single_client() + client = await async_rs_or_single_client() server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) async with server._pool.checkout() as conn: pass - self.assertEqual(1, len(server._pool.conns)) + assert len(server._pool.conns) == 1 await asyncio.sleep(1) - async with server._pool.checkout() as new_con: - self.assertEqual(conn, new_con) - self.assertEqual(1, len(server._pool.conns)) + async with server._pool.checkout() as new_conn: + assert conn == new_conn + assert len(server._pool.conns) == 1 - async def test_constants(self): + async def test_constants(self, async_client_context_fixture, simple_client): """This test uses AsyncMongoClient explicitly to make sure that host and port are not overloaded. """ - host, port = await async_client_context.host, await async_client_context.port - kwargs: dict = async_client_context.default_client_options.copy() - if async_client_context.auth_enabled: + host, port = ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + kwargs: dict = async_client_context_fixture.default_client_options.copy() + if async_client_context_fixture.auth_enabled: kwargs["username"] = db_user kwargs["password"] = db_pwd # Set bad defaults. AsyncMongoClient.HOST = "somedomainthatdoesntexist.org" AsyncMongoClient.PORT = 123456789 - with self.assertRaises(AutoReconnect): - c = self.simple_client(serverSelectionTimeoutMS=10, **kwargs) + with pytest.raises(AutoReconnect): + c = await simple_client(serverSelectionTimeoutMS=10, **kwargs) await connected(c) - - c = self.simple_client(host, port, **kwargs) + c = await simple_client(host, port, **kwargs) # Override the defaults. No error. await connected(c) - # Set good defaults. AsyncMongoClient.HOST = host AsyncMongoClient.PORT = port - # No error. - c = self.simple_client(**kwargs) + c = await simple_client(**kwargs) await connected(c) - async def test_init_disconnected(self): - host, port = await async_client_context.host, await async_client_context.port - c = await self.async_rs_or_single_client(connect=False) + async def test_init_disconnected( + self, async_client_context_fixture, async_rs_or_single_client, simple_client + ): + host, port = ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + c = await async_rs_or_single_client(connect=False) # is_primary causes client to block until connected - self.assertIsInstance(await c.is_primary, bool) - c = await self.async_rs_or_single_client(connect=False) - self.assertIsInstance(await c.is_mongos, bool) - c = await self.async_rs_or_single_client(connect=False) - self.assertIsInstance(c.options.pool_options.max_pool_size, int) - self.assertIsInstance(c.nodes, frozenset) - - c = await self.async_rs_or_single_client(connect=False) - self.assertEqual(c.codec_options, CodecOptions()) - c = await self.async_rs_or_single_client(connect=False) - self.assertFalse(await c.primary) - self.assertFalse(await c.secondaries) - c = await self.async_rs_or_single_client(connect=False) - self.assertIsInstance(c.topology_description, TopologyDescription) - self.assertEqual(c.topology_description, c._topology._description) - if async_client_context.is_rs: + assert isinstance(await c.is_primary, bool) + c = await async_rs_or_single_client(connect=False) + assert isinstance(await c.is_mongos, bool) + c = await async_rs_or_single_client(connect=False) + assert isinstance(c.options.pool_options.max_pool_size, int) + assert isinstance(c.nodes, frozenset) + + c = await async_rs_or_single_client(connect=False) + assert c.codec_options == CodecOptions() + c = await async_rs_or_single_client(connect=False) + assert not await c.primary + assert not await c.secondaries + c = await async_rs_or_single_client(connect=False) + assert isinstance(c.topology_description, TopologyDescription) + assert c.topology_description == c._topology._description + if async_client_context_fixture.is_rs: # The primary's host and port are from the replica set config. - self.assertIsNotNone(await c.address) + assert await c.address is not None else: - self.assertEqual(await c.address, (host, port)) - + assert await c.address == (host, port) bad_host = "somedomainthatdoesntexist.org" - c = self.simple_client(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10) - with self.assertRaises(ConnectionFailure): + c = await simple_client(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10) + with pytest.raises(ConnectionFailure): await c.pymongo_test.test.find_one() - async def test_init_disconnected_with_auth(self): + async def test_init_disconnected_with_auth(self, simple_client): uri = "mongodb://user:pass@somedomainthatdoesntexist" - c = self.simple_client(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10) - with self.assertRaises(ConnectionFailure): + c = await simple_client(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10) + with pytest.raises(ConnectionFailure): await c.pymongo_test.test.find_one() - async def test_equality(self): - seed = "{}:{}".format(*list(self.client._topology_settings.seeds)[0]) - c = await self.async_rs_or_single_client(seed, connect=False) - self.assertEqual(async_client_context.client, c) + async def test_equality( + self, async_client_context_fixture, async_rs_or_single_client, simple_client + ): + seed = "{}:{}".format( + *list(async_client_context_fixture.client._topology_settings.seeds)[0] + ) + c = await async_rs_or_single_client(seed, connect=False) + assert async_client_context_fixture.client == c # Explicitly test inequality - self.assertFalse(async_client_context.client != c) + assert not async_client_context_fixture.client != c - c = await self.async_rs_or_single_client("invalid.com", connect=False) - self.assertNotEqual(async_client_context.client, c) - self.assertTrue(async_client_context.client != c) + c = await async_rs_or_single_client("invalid.com", connect=False) + assert async_client_context_fixture.client != c + assert async_client_context_fixture.client != c - c1 = self.simple_client("a", connect=False) - c2 = self.simple_client("b", connect=False) + c1 = await simple_client("a", connect=False) + c2 = await simple_client("b", connect=False) # Seeds differ: - self.assertNotEqual(c1, c2) + assert c1 != c2 - c1 = self.simple_client(["a", "b", "c"], connect=False) - c2 = self.simple_client(["c", "a", "b"], connect=False) + c1 = await simple_client(["a", "b", "c"], connect=False) + c2 = await simple_client(["c", "a", "b"], connect=False) # Same seeds but out of order still compares equal: - self.assertEqual(c1, c2) - - async def test_hashable(self): - seed = "{}:{}".format(*list(self.client._topology_settings.seeds)[0]) - c = await self.async_rs_or_single_client(seed, connect=False) - self.assertIn(c, {async_client_context.client}) - c = await self.async_rs_or_single_client("invalid.com", connect=False) - self.assertNotIn(c, {async_client_context.client}) - - async def test_host_w_port(self): - with self.assertRaises(ValueError): - host = await async_client_context.host + assert c1 == c2 + + async def test_hashable(self, async_client_context_fixture, async_rs_or_single_client): + seed = "{}:{}".format( + *list(async_client_context_fixture.client._topology_settings.seeds)[0] + ) + c = await async_rs_or_single_client(seed, connect=False) + assert c in {async_client_context_fixture.client} + c = await async_rs_or_single_client("invalid.com", connect=False) + assert c not in {async_client_context_fixture.client} + + async def test_host_w_port(self, async_client_context_fixture): + with pytest.raises(ValueError): + host = await async_client_context_fixture.host await connected( AsyncMongoClient( f"{host}:1234567", @@ -887,7 +923,7 @@ async def test_host_w_port(self): ) ) - async def test_repr(self): + async def test_repr(self, simple_client): # Used to test 'eval' below. import bson @@ -897,19 +933,16 @@ async def test_repr(self): connect=False, document_class=SON, ) - the_repr = repr(client) - self.assertIn("AsyncMongoClient(host=", the_repr) - self.assertIn("document_class=bson.son.SON, tz_aware=False, connect=False, ", the_repr) - self.assertIn("connecttimeoutms=12345", the_repr) - self.assertIn("replicaset='replset'", the_repr) - self.assertIn("w=1", the_repr) - self.assertIn("wtimeoutms=100", the_repr) - + assert "AsyncMongoClient(host=" in the_repr + assert "document_class=bson.son.SON, tz_aware=False, connect=False, " in the_repr + assert "connecttimeoutms=12345" in the_repr + assert "replicaset='replset'" in the_repr + assert "w=1" in the_repr + assert "wtimeoutms=100" in the_repr async with eval(the_repr) as client_two: - self.assertEqual(client_two, client) - - client = self.simple_client( + assert client_two == client + client = await simple_client( "localhost:27017,localhost:27018", replicaSet="replset", connectTimeoutMS=12345, @@ -919,93 +952,104 @@ async def test_repr(self): connect=False, ) the_repr = repr(client) - self.assertIn("AsyncMongoClient(host=", the_repr) - self.assertIn("document_class=dict, tz_aware=False, connect=False, ", the_repr) - self.assertIn("connecttimeoutms=12345", the_repr) - self.assertIn("replicaset='replset'", the_repr) - self.assertIn("sockettimeoutms=None", the_repr) - self.assertIn("w=1", the_repr) - self.assertIn("wtimeoutms=100", the_repr) - + assert "AsyncMongoClient(host=" in the_repr + assert "document_class=dict, tz_aware=False, connect=False, " in the_repr + assert "connecttimeoutms=12345" in the_repr + assert "replicaset='replset'" in the_repr + assert "sockettimeoutms=None" in the_repr + assert "w=1" in the_repr + assert "wtimeoutms=100" in the_repr async with eval(the_repr) as client_two: - self.assertEqual(client_two, client) + assert client_two == client - async def test_getters(self): + async def test_getters(self, async_client_context_fixture): await async_wait_until( - lambda: async_client_context.nodes == self.client.nodes, "find all nodes" + lambda: async_client_context_fixture.nodes == async_client_context_fixture.client.nodes, + "find all nodes", ) - async def test_list_databases(self): - cmd_docs = (await self.client.admin.command("listDatabases"))["databases"] - cursor = await self.client.list_databases() - self.assertIsInstance(cursor, AsyncCommandCursor) + async def test_list_databases(self, async_client_context_fixture, async_rs_or_single_client): + cmd_docs = (await async_client_context_fixture.client.admin.command("listDatabases"))[ + "databases" + ] + cursor = await async_client_context_fixture.client.list_databases() + assert isinstance(cursor, AsyncCommandCursor) helper_docs = await cursor.to_list() - self.assertTrue(len(helper_docs) > 0) - self.assertEqual(len(helper_docs), len(cmd_docs)) + assert len(helper_docs) > 0 + assert len(helper_docs) == len(cmd_docs) # PYTHON-3529 Some fields may change between calls, just compare names. for helper_doc, cmd_doc in zip(helper_docs, cmd_docs): - self.assertIs(type(helper_doc), dict) - self.assertEqual(helper_doc.keys(), cmd_doc.keys()) - client = await self.async_rs_or_single_client(document_class=SON) - async for doc in await client.list_databases(): - self.assertIs(type(doc), dict) - - await self.client.pymongo_test.test.insert_one({}) - cursor = await self.client.list_databases(filter={"name": "admin"}) + assert isinstance(helper_doc, dict) + assert helper_doc.keys() == cmd_doc.keys() + + client_doc = await async_rs_or_single_client(document_class=SON) + async for doc in await client_doc.list_databases(): + assert isinstance(doc, dict) + + await async_client_context_fixture.client.pymongo_test.test.insert_one({}) + cursor = await async_client_context_fixture.client.list_databases(filter={"name": "admin"}) docs = await cursor.to_list() - self.assertEqual(1, len(docs)) - self.assertEqual(docs[0]["name"], "admin") + assert len(docs) == 1 + assert docs[0]["name"] == "admin" - cursor = await self.client.list_databases(nameOnly=True) + cursor = await async_client_context_fixture.client.list_databases(nameOnly=True) async for doc in cursor: - self.assertEqual(["name"], list(doc)) + assert list(doc) == ["name"] - async def test_list_database_names(self): - await self.client.pymongo_test.test.insert_one({"dummy": "object"}) - await self.client.pymongo_test_mike.test.insert_one({"dummy": "object"}) - cmd_docs = (await self.client.admin.command("listDatabases"))["databases"] + async def test_list_database_names(self, async_client_context_fixture): + await async_client_context_fixture.client.pymongo_test.test.insert_one({"dummy": "object"}) + await async_client_context_fixture.client.pymongo_test_mike.test.insert_one( + {"dummy": "object"} + ) + cmd_docs = (await async_client_context_fixture.client.admin.command("listDatabases"))[ + "databases" + ] cmd_names = [doc["name"] for doc in cmd_docs] - db_names = await self.client.list_database_names() - self.assertTrue("pymongo_test" in db_names) - self.assertTrue("pymongo_test_mike" in db_names) - self.assertEqual(db_names, cmd_names) - - async def test_drop_database(self): - with self.assertRaises(TypeError): - await self.client.drop_database(5) # type: ignore[arg-type] - with self.assertRaises(TypeError): - await self.client.drop_database(None) # type: ignore[arg-type] - - await self.client.pymongo_test.test.insert_one({"dummy": "object"}) - await self.client.pymongo_test2.test.insert_one({"dummy": "object"}) - dbs = await self.client.list_database_names() - self.assertIn("pymongo_test", dbs) - self.assertIn("pymongo_test2", dbs) - await self.client.drop_database("pymongo_test") - - if async_client_context.is_rs: - wc_client = await self.async_rs_or_single_client(w=len(async_client_context.nodes) + 1) - with self.assertRaises(WriteConcernError): + db_names = await async_client_context_fixture.client.list_database_names() + assert "pymongo_test" in db_names + assert "pymongo_test_mike" in db_names + assert db_names == cmd_names + + async def test_drop_database(self, async_client_context_fixture, async_rs_or_single_client): + with pytest.raises(TypeError): + await async_client_context_fixture.client.drop_database(5) # type: ignore[arg-type] + with pytest.raises(TypeError): + await async_client_context_fixture.client.drop_database(None) # type: ignore[arg-type] + + await async_client_context_fixture.client.pymongo_test.test.insert_one({"dummy": "object"}) + await async_client_context_fixture.client.pymongo_test2.test.insert_one({"dummy": "object"}) + dbs = await async_client_context_fixture.client.list_database_names() + assert "pymongo_test" in dbs + assert "pymongo_test2" in dbs + await async_client_context_fixture.client.drop_database("pymongo_test") + + if async_client_context_fixture.is_rs: + wc_client = await async_rs_or_single_client( + w=len(async_client_context_fixture.nodes) + 1 + ) + with pytest.raises(WriteConcernError): await wc_client.drop_database("pymongo_test2") - await self.client.drop_database(self.client.pymongo_test2) - dbs = await self.client.list_database_names() - self.assertNotIn("pymongo_test", dbs) - self.assertNotIn("pymongo_test2", dbs) + await async_client_context_fixture.client.drop_database( + async_client_context_fixture.client.pymongo_test2 + ) + dbs = await async_client_context_fixture.client.list_database_names() + assert "pymongo_test" not in dbs + assert "pymongo_test2" not in dbs - async def test_close(self): - test_client = await self.async_rs_or_single_client() + async def test_close(self, async_rs_or_single_client): + test_client = await async_rs_or_single_client() coll = test_client.pymongo_test.bar await test_client.close() - with self.assertRaises(InvalidOperation): + with pytest.raises(InvalidOperation): await coll.count_documents({}) - async def test_close_kills_cursors(self): + async def test_close_kills_cursors(self, async_rs_or_single_client): if sys.platform.startswith("java"): # We can't figure out how to make this test reliable with Jython. raise SkipTest("Can't test with Jython") - test_client = await self.async_rs_or_single_client() + test_client = await async_rs_or_single_client() # Kill any cursors possibly queued up by previous tests. gc.collect() await test_client._process_periodic_tasks() @@ -1017,248 +1061,264 @@ async def test_close_kills_cursors(self): # Open a cursor and leave it open on the server. cursor = coll.find().batch_size(10) - self.assertTrue(bool(await anext(cursor))) - self.assertLess(cursor.retrieved, docs_inserted) + assert bool(await anext(cursor)) + assert cursor.retrieved < docs_inserted # Open a command cursor and leave it open on the server. cursor = await coll.aggregate([], batchSize=10) - self.assertTrue(bool(await anext(cursor))) + assert bool(await anext(cursor)) del cursor # Required for PyPy, Jython and other Python implementations that # don't use reference counting garbage collection. gc.collect() # Close the client and ensure the topology is closed. - self.assertTrue(test_client._topology._opened) + assert test_client._topology._opened await test_client.close() - self.assertFalse(test_client._topology._opened) - test_client = await self.async_rs_or_single_client() + assert not test_client._topology._opened + test_client = await async_rs_or_single_client() # The killCursors task should not need to re-open the topology. await test_client._process_periodic_tasks() - self.assertTrue(test_client._topology._opened) + assert test_client._topology._opened - async def test_close_stops_kill_cursors_thread(self): - client = await self.async_rs_client() + async def test_close_stops_kill_cursors_thread(self, async_rs_client): + client = await async_rs_client() await client.test.test.find_one() - self.assertFalse(client._kill_cursors_executor._stopped) + assert not client._kill_cursors_executor._stopped # Closing the client should stop the thread. await client.close() - self.assertTrue(client._kill_cursors_executor._stopped) + assert client._kill_cursors_executor._stopped # Reusing the closed client should raise an InvalidOperation error. - with self.assertRaises(InvalidOperation): + with pytest.raises(InvalidOperation): await client.admin.command("ping") # Thread is still stopped. - self.assertTrue(client._kill_cursors_executor._stopped) + assert client._kill_cursors_executor._stopped - async def test_uri_connect_option(self): + async def test_uri_connect_option(self, async_rs_client): # Ensure that topology is not opened if connect=False. - client = await self.async_rs_client(connect=False) - self.assertFalse(client._topology._opened) + client = await async_rs_client(connect=False) + assert not client._topology._opened # Ensure kill cursors thread has not been started. if _IS_SYNC: kc_thread = client._kill_cursors_executor._thread - self.assertFalse(kc_thread and kc_thread.is_alive()) + assert not (kc_thread and kc_thread.is_alive()) else: kc_task = client._kill_cursors_executor._task - self.assertFalse(kc_task and not kc_task.done()) + assert not (kc_task and not kc_task.done()) # Using the client should open topology and start the thread. await client.admin.command("ping") - self.assertTrue(client._topology._opened) + assert client._topology._opened if _IS_SYNC: kc_thread = client._kill_cursors_executor._thread - self.assertTrue(kc_thread and kc_thread.is_alive()) + assert kc_thread and kc_thread.is_alive() else: kc_task = client._kill_cursors_executor._task - self.assertTrue(kc_task and not kc_task.done()) + assert kc_task and not kc_task.done() - async def test_close_does_not_open_servers(self): - client = await self.async_rs_client(connect=False) + async def test_close_does_not_open_servers(self, async_rs_client): + client = await async_rs_client(connect=False) topology = client._topology - self.assertEqual(topology._servers, {}) + assert topology._servers == {} await client.close() - self.assertEqual(topology._servers, {}) + assert topology._servers == {} - async def test_close_closes_sockets(self): - client = await self.async_rs_client() + async def test_close_closes_sockets(self, async_rs_client): + client = await async_rs_client() await client.test.test.find_one() topology = client._topology await client.close() for server in topology._servers.values(): - self.assertFalse(server._pool.conns) - self.assertTrue(server._monitor._executor._stopped) - self.assertTrue(server._monitor._rtt_monitor._executor._stopped) - self.assertFalse(server._monitor._pool.conns) - self.assertFalse(server._monitor._rtt_monitor._pool.conns) - - def test_bad_uri(self): - with self.assertRaises(InvalidURI): + assert not server._pool.conns + assert server._monitor._executor._stopped + assert server._monitor._rtt_monitor._executor._stopped + assert not server._monitor._pool.conns + assert not server._monitor._rtt_monitor._pool.conns + + async def test_bad_uri(self): + with pytest.raises(InvalidURI): AsyncMongoClient("http://localhost") - @async_client_context.require_auth - @async_client_context.require_no_fips - async def test_auth_from_uri(self): - host, port = await async_client_context.host, await async_client_context.port - await async_client_context.create_user("admin", "admin", "pass") - self.addAsyncCleanup(async_client_context.drop_user, "admin", "admin") - self.addAsyncCleanup(remove_all_users, self.client.pymongo_test) - - await async_client_context.create_user( + @pytest.mark.usefixtures("require_auth") + @pytest.mark.usefixtures("require_no_fips") + @pytest.mark.parametrize("remove_all_users_fixture", ["pymongo_test"], indirect=True) + @pytest.mark.parametrize("drop_user_fixture", [("admin", "admin")], indirect=True) + async def test_auth_from_uri( + self, + async_client_context_fixture, + async_rs_or_single_client_noauth, + remove_all_users_fixture, + drop_user_fixture, + ): + host, port = ( + await async_client_context_fixture.host, + await async_client_context_fixture.port, + ) + await async_client_context_fixture.create_user("admin", "admin", "pass") + + await async_client_context_fixture.create_user( "pymongo_test", "user", "pass", roles=["userAdmin", "readWrite"] ) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): await connected( - await self.async_rs_or_single_client_noauth("mongodb://a:b@%s:%d" % (host, port)) + await async_rs_or_single_client_noauth("mongodb://a:b@%s:%d" % (host, port)) ) # No error. await connected( - await self.async_rs_or_single_client_noauth("mongodb://admin:pass@%s:%d" % (host, port)) + await async_rs_or_single_client_noauth("mongodb://admin:pass@%s:%d" % (host, port)) ) # Wrong database. uri = "mongodb://admin:pass@%s:%d/pymongo_test" % (host, port) - with self.assertRaises(OperationFailure): - await connected(await self.async_rs_or_single_client_noauth(uri)) + with pytest.raises(OperationFailure): + await connected(await async_rs_or_single_client_noauth(uri)) # No error. await connected( - await self.async_rs_or_single_client_noauth( + await async_rs_or_single_client_noauth( "mongodb://user:pass@%s:%d/pymongo_test" % (host, port) ) ) # Auth with lazy connection. await ( - await self.async_rs_or_single_client_noauth( + await async_rs_or_single_client_noauth( "mongodb://user:pass@%s:%d/pymongo_test" % (host, port), connect=False ) ).pymongo_test.test.find_one() # Wrong password. - bad_client = await self.async_rs_or_single_client_noauth( + bad_client = await async_rs_or_single_client_noauth( "mongodb://user:wrong@%s:%d/pymongo_test" % (host, port), connect=False ) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): await bad_client.pymongo_test.test.find_one() - @async_client_context.require_auth - async def test_username_and_password(self): - await async_client_context.create_user("admin", "ad min", "pa/ss") - self.addAsyncCleanup(async_client_context.drop_user, "admin", "ad min") + @pytest.mark.usefixtures("require_auth") + @pytest.mark.parametrize("drop_user_fixture", [("admin", "ad min")], indirect=True) + async def test_username_and_password( + self, async_client_context_fixture, async_rs_or_single_client_noauth, drop_user_fixture + ): + await async_client_context_fixture.create_user("admin", "ad min", "pa/ss") - c = await self.async_rs_or_single_client_noauth(username="ad min", password="pa/ss") + c = await async_rs_or_single_client_noauth(username="ad min", password="pa/ss") # Username and password aren't in strings that will likely be logged. - self.assertNotIn("ad min", repr(c)) - self.assertNotIn("ad min", str(c)) - self.assertNotIn("pa/ss", repr(c)) - self.assertNotIn("pa/ss", str(c)) + assert "ad min" not in repr(c) + assert "ad min" not in str(c) + assert "pa/ss" not in repr(c) + assert "pa/ss" not in str(c) # Auth succeeds. await c.server_info() - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): await ( - await self.async_rs_or_single_client_noauth(username="ad min", password="foo") + await async_rs_or_single_client_noauth(username="ad min", password="foo") ).server_info() - @async_client_context.require_auth - @async_client_context.require_no_fips - async def test_lazy_auth_raises_operation_failure(self): - host = await async_client_context.host - lazy_client = await self.async_rs_or_single_client_noauth( + @pytest.mark.usefixtures("require_auth") + @pytest.mark.usefixtures("require_no_fips") + async def test_lazy_auth_raises_operation_failure( + self, async_client_context_fixture, async_rs_or_single_client_noauth + ): + host = await async_client_context_fixture.host + lazy_client = await async_rs_or_single_client_noauth( f"mongodb://user:wrong@{host}/pymongo_test", connect=False ) await asyncAssertRaisesExactly(OperationFailure, lazy_client.test.collection.find_one) - @async_client_context.require_no_tls - async def test_unix_socket(self): + @pytest.mark.usefixtures("require_no_tls") + async def test_unix_socket( + self, async_client_context_fixture, async_rs_or_single_client, simple_client + ): if not hasattr(socket, "AF_UNIX"): - raise SkipTest("UNIX-sockets are not supported on this system") + pytest.skip("UNIX-sockets are not supported on this system") - mongodb_socket = "/tmp/mongodb-%d.sock" % (await async_client_context.port,) - encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (await async_client_context.port,) + mongodb_socket = "/tmp/mongodb-%d.sock" % (await async_client_context_fixture.port,) + encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % ( + await async_client_context_fixture.port, + ) if not os.access(mongodb_socket, os.R_OK): - raise SkipTest("Socket file is not accessible") + pytest.skip("Socket file is not accessible") uri = "mongodb://%s" % encoded_socket # Confirm we can do operations via the socket. - client = await self.async_rs_or_single_client(uri) + client = await async_rs_or_single_client(uri) await client.pymongo_test.test.insert_one({"dummy": "object"}) dbs = await client.list_database_names() - self.assertTrue("pymongo_test" in dbs) + assert "pymongo_test" in dbs - self.assertTrue(mongodb_socket in repr(client)) + assert mongodb_socket in repr(client) # Confirm it fails with a missing socket. - with self.assertRaises(ConnectionFailure): - c = self.simple_client( + with pytest.raises(ConnectionFailure): + c = await simple_client( "mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100 ) await connected(c) - async def test_document_class(self): - c = self.client + async def test_document_class(self, async_client_context_fixture, async_rs_or_single_client): + c = async_client_context_fixture.client db = c.pymongo_test await db.test.insert_one({"x": 1}) - self.assertEqual(dict, c.codec_options.document_class) - self.assertTrue(isinstance(await db.test.find_one(), dict)) - self.assertFalse(isinstance(await db.test.find_one(), SON)) + assert dict == c.codec_options.document_class + assert isinstance(await db.test.find_one(), dict) + assert not isinstance(await db.test.find_one(), SON) - c = await self.async_rs_or_single_client(document_class=SON) + c = await async_rs_or_single_client(document_class=SON) db = c.pymongo_test - self.assertEqual(SON, c.codec_options.document_class) - self.assertTrue(isinstance(await db.test.find_one(), SON)) + assert SON == c.codec_options.document_class + assert isinstance(await db.test.find_one(), SON) - async def test_timeouts(self): - client = await self.async_rs_or_single_client( + async def test_timeouts(self, async_rs_or_single_client): + client = await async_rs_or_single_client( connectTimeoutMS=10500, socketTimeoutMS=10500, maxIdleTimeMS=10500, serverSelectionTimeoutMS=10500, ) - self.assertEqual(10.5, (await async_get_pool(client)).opts.connect_timeout) - self.assertEqual(10.5, (await async_get_pool(client)).opts.socket_timeout) - self.assertEqual(10.5, (await async_get_pool(client)).opts.max_idle_time_seconds) - self.assertEqual(10.5, client.options.pool_options.max_idle_time_seconds) - self.assertEqual(10.5, client.options.server_selection_timeout) + assert 10.5 == (await async_get_pool(client)).opts.connect_timeout + assert 10.5 == (await async_get_pool(client)).opts.socket_timeout + assert 10.5 == (await async_get_pool(client)).opts.max_idle_time_seconds + assert 10.5 == client.options.pool_options.max_idle_time_seconds + assert 10.5 == client.options.server_selection_timeout - async def test_socket_timeout_ms_validation(self): - c = await self.async_rs_or_single_client(socketTimeoutMS=10 * 1000) - self.assertEqual(10, (await async_get_pool(c)).opts.socket_timeout) + async def test_socket_timeout_ms_validation(self, async_rs_or_single_client): + c = await async_rs_or_single_client(socketTimeoutMS=10 * 1000) + assert 10 == (await async_get_pool(c)).opts.socket_timeout - c = await connected(await self.async_rs_or_single_client(socketTimeoutMS=None)) - self.assertEqual(None, (await async_get_pool(c)).opts.socket_timeout) + c = await connected(await async_rs_or_single_client(socketTimeoutMS=None)) + assert (await async_get_pool(c)).opts.socket_timeout is None - c = await connected(await self.async_rs_or_single_client(socketTimeoutMS=0)) - self.assertEqual(None, (await async_get_pool(c)).opts.socket_timeout) + c = await connected(await async_rs_or_single_client(socketTimeoutMS=0)) + assert (await async_get_pool(c)).opts.socket_timeout is None - with self.assertRaises(ValueError): - async with await self.async_rs_or_single_client(socketTimeoutMS=-1): + with pytest.raises(ValueError): + async with await async_rs_or_single_client(socketTimeoutMS=-1): pass - with self.assertRaises(ValueError): - async with await self.async_rs_or_single_client(socketTimeoutMS=1e10): + with pytest.raises(ValueError): + async with await async_rs_or_single_client(socketTimeoutMS=1e10): pass - with self.assertRaises(ValueError): - async with await self.async_rs_or_single_client(socketTimeoutMS="foo"): + with pytest.raises(ValueError): + async with await async_rs_or_single_client(socketTimeoutMS="foo"): pass - async def test_socket_timeout(self): - no_timeout = self.client + async def test_socket_timeout(self, async_client_context_fixture, async_rs_or_single_client): + no_timeout = async_client_context_fixture.client timeout_sec = 1 - timeout = await self.async_rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) - self.addAsyncCleanup(timeout.close) + timeout = await async_rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) await no_timeout.pymongo_test.drop_collection("test") await no_timeout.pymongo_test.test.insert_one({"x": 1}) @@ -1270,24 +1330,22 @@ async def get_x(db): doc = await anext(db.test.find().where(where_func)) return doc["x"] - self.assertEqual(1, await get_x(no_timeout.pymongo_test)) - with self.assertRaises(NetworkTimeout): + assert 1 == await get_x(no_timeout.pymongo_test) + with pytest.raises(NetworkTimeout): await get_x(timeout.pymongo_test) async def test_server_selection_timeout(self): client = AsyncMongoClient(serverSelectionTimeoutMS=100, connect=False) - self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0.1) await client.close() client = AsyncMongoClient(serverSelectionTimeoutMS=0, connect=False) - self.assertAlmostEqual(0, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0) - self.assertRaises( - ValueError, AsyncMongoClient, serverSelectionTimeoutMS="foo", connect=False - ) - self.assertRaises(ValueError, AsyncMongoClient, serverSelectionTimeoutMS=-1, connect=False) - self.assertRaises( + pytest.raises(ValueError, AsyncMongoClient, serverSelectionTimeoutMS="foo", connect=False) + pytest.raises(ValueError, AsyncMongoClient, serverSelectionTimeoutMS=-1, connect=False) + pytest.raises( ConfigurationError, AsyncMongoClient, serverSelectionTimeoutMS=None, connect=False ) await client.close() @@ -1295,108 +1353,106 @@ async def test_server_selection_timeout(self): client = AsyncMongoClient( "mongodb://localhost/?serverSelectionTimeoutMS=100", connect=False ) - self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0.1) await client.close() client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=0", connect=False) - self.assertAlmostEqual(0, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0) await client.close() # Test invalid timeout in URI ignored and set to default. client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=-1", connect=False) - self.assertAlmostEqual(30, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 30) await client.close() client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=", connect=False) - self.assertAlmostEqual(30, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 30) - async def test_waitQueueTimeoutMS(self): - client = await self.async_rs_or_single_client(waitQueueTimeoutMS=2000) - self.assertEqual((await async_get_pool(client)).opts.wait_queue_timeout, 2) + async def test_waitQueueTimeoutMS(self, async_rs_or_single_client): + client = await async_rs_or_single_client(waitQueueTimeoutMS=2000) + assert 2 == (await async_get_pool(client)).opts.wait_queue_timeout - async def test_socketKeepAlive(self): - pool = await async_get_pool(self.client) + async def test_socketKeepAlive(self, async_client_context_fixture): + pool = await async_get_pool(async_client_context_fixture.client) async with pool.checkout() as conn: keepalive = conn.conn.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) - self.assertTrue(keepalive) + assert keepalive @no_type_check - async def test_tz_aware(self): - self.assertRaises(ValueError, AsyncMongoClient, tz_aware="foo") + async def test_tz_aware(self, async_client_context_fixture, async_rs_or_single_client): + pytest.raises(ValueError, AsyncMongoClient, tz_aware="foo") - aware = await self.async_rs_or_single_client(tz_aware=True) - self.addAsyncCleanup(aware.close) - naive = self.client + aware = await async_rs_or_single_client(tz_aware=True) + naive = async_client_context_fixture.client await aware.pymongo_test.drop_collection("test") now = datetime.datetime.now(tz=datetime.timezone.utc) await aware.pymongo_test.test.insert_one({"x": now}) - self.assertEqual(None, (await naive.pymongo_test.test.find_one())["x"].tzinfo) - self.assertEqual(utc, (await aware.pymongo_test.test.find_one())["x"].tzinfo) - self.assertEqual( - (await aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None), - (await naive.pymongo_test.test.find_one())["x"], - ) + assert (await naive.pymongo_test.test.find_one())["x"].tzinfo is None + assert utc == (await aware.pymongo_test.test.find_one())["x"].tzinfo + assert (await aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None) == ( + await naive.pymongo_test.test.find_one() + )["x"] - @async_client_context.require_ipv6 - async def test_ipv6(self): - if async_client_context.tls: + @pytest.mark.usefixtures("require_ipv6") + async def test_ipv6(self, async_client_context_fixture, async_rs_or_single_client_noauth): + if async_client_context_fixture.tls: if not HAVE_IPADDRESS: - raise SkipTest("Need the ipaddress module to test with SSL") + pytest.skip("Need the ipaddress module to test with SSL") - if async_client_context.auth_enabled: + if async_client_context_fixture.auth_enabled: auth_str = f"{db_user}:{db_pwd}@" else: auth_str = "" - uri = "mongodb://%s[::1]:%d" % (auth_str, await async_client_context.port) - if async_client_context.is_rs: - uri += "/?replicaSet=" + (async_client_context.replica_set_name or "") + uri = "mongodb://%s[::1]:%d" % (auth_str, await async_client_context_fixture.port) + if async_client_context_fixture.is_rs: + uri += "/?replicaSet=" + (async_client_context_fixture.replica_set_name or "") - client = await self.async_rs_or_single_client_noauth(uri) + client = await async_rs_or_single_client_noauth(uri) await client.pymongo_test.test.insert_one({"dummy": "object"}) await client.pymongo_test_bernie.test.insert_one({"dummy": "object"}) dbs = await client.list_database_names() - self.assertTrue("pymongo_test" in dbs) - self.assertTrue("pymongo_test_bernie" in dbs) + assert "pymongo_test" in dbs + assert "pymongo_test_bernie" in dbs - async def test_contextlib(self): - client = await self.async_rs_or_single_client() + async def test_contextlib(self, async_rs_or_single_client): + client = await async_rs_or_single_client() await client.pymongo_test.drop_collection("test") await client.pymongo_test.test.insert_one({"foo": "bar"}) # The socket used for the previous commands has been returned to the # pool - self.assertEqual(1, len((await async_get_pool(client)).conns)) + assert 1 == len((await async_get_pool(client)).conns) # contextlib async support was added in Python 3.10 if _IS_SYNC or sys.version_info >= (3, 10): async with contextlib.aclosing(client): - self.assertEqual("bar", (await client.pymongo_test.test.find_one())["foo"]) - with self.assertRaises(InvalidOperation): + assert "bar" == (await client.pymongo_test.test.find_one())["foo"] + with pytest.raises(InvalidOperation): await client.pymongo_test.test.find_one() - client = await self.async_rs_or_single_client() + client = await async_rs_or_single_client() async with client as client: - self.assertEqual("bar", (await client.pymongo_test.test.find_one())["foo"]) - with self.assertRaises(InvalidOperation): + assert "bar" == (await client.pymongo_test.test.find_one())["foo"] + with pytest.raises(InvalidOperation): await client.pymongo_test.test.find_one() - @async_client_context.require_sync - def test_interrupt_signal(self): + @pytest.mark.usefixtures("require_sync") + def test_interrupt_signal(self, async_client_context_fixture): if sys.platform.startswith("java"): # We can't figure out how to raise an exception on a thread that's # blocked on a socket, whether that's the main thread or a worker, # without simply killing the whole thread in Jython. This suggests # PYTHON-294 can't actually occur in Jython. - raise SkipTest("Can't test interrupts in Jython") + pytest.skip("Can't test interrupts in Jython") if is_greenthread_patched(): - raise SkipTest("Can't reliably test interrupts with green threads") + pytest.skip("Can't reliably test interrupts with green threads") # Test fix for PYTHON-294 -- make sure AsyncMongoClient closes its # socket if it gets an interrupt while waiting to recv() from it. - db = self.client.pymongo_test + db = async_client_context_fixture.client.pymongo_test # A $where clause which takes 1.5 sec to execute where = delay(1.5) @@ -1438,48 +1494,48 @@ def sigalarm(num, frame): except KeyboardInterrupt: raised = True - # Can't use self.assertRaises() because it doesn't catch system - # exceptions - self.assertTrue(raised, "Didn't raise expected KeyboardInterrupt") + assert raised, "Didn't raise expected KeyboardInterrupt" # Raises AssertionError due to PYTHON-294 -- Mongo's response to # the previous find() is still waiting to be read on the socket, # so the request id's don't match. - self.assertEqual({"_id": 1}, next(db.foo.find())) # type: ignore[call-overload] + assert {"_id": 1} == next(db.foo.find()) # type: ignore[call-overload] finally: if old_signal_handler: signal.signal(signal.SIGALRM, old_signal_handler) - async def test_operation_failure(self): + async def test_operation_failure(self, async_single_client): # Ensure AsyncMongoClient doesn't close socket after it gets an error # response to getLastError. PYTHON-395. We need a new client here # to avoid race conditions caused by replica set failover or idle # socket reaping. - client = await self.async_single_client() + client = await async_single_client() await client.pymongo_test.test.find_one() pool = await async_get_pool(client) socket_count = len(pool.conns) - self.assertGreaterEqual(socket_count, 1) + assert socket_count >= 1 old_conn = next(iter(pool.conns)) await client.pymongo_test.test.drop() await client.pymongo_test.test.insert_one({"_id": "foo"}) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): await client.pymongo_test.test.insert_one({"_id": "foo"}) - self.assertEqual(socket_count, len(pool.conns)) - new_con = next(iter(pool.conns)) - self.assertEqual(old_conn, new_con) + assert socket_count == len(pool.conns) + new_conn = next(iter(pool.conns)) + assert old_conn == new_conn - async def test_lazy_connect_w0(self): + @pytest.mark.parametrize("drop_database_fixture", ["test_lazy_connect_w0"], indirect=True) + async def test_lazy_connect_w0( + self, async_client_context_fixture, async_rs_or_single_client, drop_database_fixture + ): # Ensure that connect-on-demand works when the first operation is # an unacknowledged write. This exercises _writable_max_wire_version(). # Use a separate collection to avoid races where we're still # completing an operation on a collection while the next test begins. - await async_client_context.client.drop_database("test_lazy_connect_w0") - self.addAsyncCleanup(async_client_context.client.drop_database, "test_lazy_connect_w0") + await async_client_context_fixture.client.drop_database("test_lazy_connect_w0") - client = await self.async_rs_or_single_client(connect=False, w=0) + client = await async_rs_or_single_client(connect=False, w=0) await client.test_lazy_connect_w0.test.insert_one({}) async def predicate(): @@ -1487,7 +1543,7 @@ async def predicate(): await async_wait_until(predicate, "find one document") - client = await self.async_rs_or_single_client(connect=False, w=0) + client = await async_rs_or_single_client(connect=False, w=0) await client.test_lazy_connect_w0.test.update_one({}, {"$set": {"x": 1}}) async def predicate(): @@ -1495,7 +1551,7 @@ async def predicate(): await async_wait_until(predicate, "update one document") - client = await self.async_rs_or_single_client(connect=False, w=0) + client = await async_rs_or_single_client(connect=False, w=0) await client.test_lazy_connect_w0.test.delete_one({}) async def predicate(): @@ -1503,11 +1559,11 @@ async def predicate(): await async_wait_until(predicate, "delete one document") - @async_client_context.require_no_mongos - async def test_exhaust_network_error(self): + @pytest.mark.usefixtures("require_no_mongos") + async def test_exhaust_network_error(self, async_rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid semaphore leaks. - client = await self.async_rs_or_single_client(maxPoolSize=1, retryReads=False) + client = await async_rs_or_single_client(maxPoolSize=1, retryReads=False) collection = client.pymongo_test.test pool = await async_get_pool(client) pool._check_interval_seconds = None # Never check. @@ -1519,24 +1575,22 @@ async def test_exhaust_network_error(self): conn = one(pool.conns) conn.conn.close() cursor = collection.find(cursor_type=CursorType.EXHAUST) - with self.assertRaises(ConnectionFailure): + with pytest.raises(ConnectionFailure): await anext(cursor) - self.assertTrue(conn.closed) + assert conn.closed # The semaphore was decremented despite the error. - self.assertEqual(0, pool.requests) + assert 0 == pool.requests - @async_client_context.require_auth - async def test_auth_network_error(self): + @pytest.mark.usefixtures("require_auth") + async def test_auth_network_error(self, async_rs_or_single_client): # Make sure there's no semaphore leak if we get a network error # when authenticating a new socket with cached credentials. # Get a client with one socket so we detect if it's leaked. c = await connected( - await self.async_rs_or_single_client( - maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False - ) + await async_rs_or_single_client(maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False) ) # Cause a network error on the actual socket. @@ -1546,25 +1600,25 @@ async def test_auth_network_error(self): # AsyncConnection.authenticate logs, but gets a socket.error. Should be # reraised as AutoReconnect. - with self.assertRaises(AutoReconnect): + with pytest.raises(AutoReconnect): await c.test.collection.find_one() # No semaphore leak, the pool is allowed to make a new socket. await c.test.collection.find_one() - @async_client_context.require_no_replica_set - async def test_connect_to_standalone_using_replica_set_name(self): - client = await self.async_single_client(replicaSet="anything", serverSelectionTimeoutMS=100) - with self.assertRaises(AutoReconnect): + @pytest.mark.usefixtures("require_no_replica_set") + async def test_connect_to_standalone_using_replica_set_name(self, async_single_client): + client = await async_single_client(replicaSet="anything", serverSelectionTimeoutMS=100) + with pytest.raises(AutoReconnect): await client.test.test.find_one() - @async_client_context.require_replica_set - async def test_stale_getmore(self): + @pytest.mark.usefixtures("require_replica_set") + async def test_stale_getmore(self, async_rs_client): # A cursor is created, but its member goes down and is removed from # the topology before the getMore message is sent. Test that # AsyncMongoClient._run_operation_with_response handles the error. - with self.assertRaises(AutoReconnect): - client = await self.async_rs_client(connect=False, serverSelectionTimeoutMS=100) + with pytest.raises(AutoReconnect): + client = await async_rs_client(connect=False, serverSelectionTimeoutMS=100) await client._run_operation( operation=message._GetMore( "pymongo_test", @@ -1584,7 +1638,7 @@ async def test_stale_getmore(self): address=("not-a-member", 27017), ) - async def test_heartbeat_frequency_ms(self): + async def test_heartbeat_frequency_ms(self, async_client_context_fixture, async_single_client): class HeartbeatStartedListener(ServerHeartbeatListener): def __init__(self): self.results = [] @@ -1609,116 +1663,129 @@ def init(self, *args): ServerHeartbeatStartedEvent.__init__ = init # type: ignore listener = HeartbeatStartedListener() uri = "mongodb://%s:%d/?heartbeatFrequencyMS=500" % ( - await async_client_context.host, - await async_client_context.port, + await async_client_context_fixture.host, + await async_client_context_fixture.port, ) - await self.async_single_client(uri, event_listeners=[listener]) + await async_single_client(uri, event_listeners=[listener]) await async_wait_until( lambda: len(listener.results) >= 2, "record two ServerHeartbeatStartedEvents" ) # Default heartbeatFrequencyMS is 10 sec. Check the interval was # closer to 0.5 sec with heartbeatFrequencyMS configured. - self.assertAlmostEqual(heartbeat_times[1] - heartbeat_times[0], 0.5, delta=2) + pytest.approx(heartbeat_times[1] - heartbeat_times[0], 0.5, abs=2) finally: ServerHeartbeatStartedEvent.__init__ = old_init # type: ignore - def test_small_heartbeat_frequency_ms(self): + async def test_small_heartbeat_frequency_ms(self): uri = "mongodb://example/?heartbeatFrequencyMS=499" - with self.assertRaises(ConfigurationError) as context: + with pytest.raises(ConfigurationError) as context: AsyncMongoClient(uri) - self.assertIn("heartbeatFrequencyMS", str(context.exception)) + assert "heartbeatFrequencyMS" in str(context.value) - async def test_compression(self): + async def test_compression( + self, async_client_context_fixture, simple_client, async_single_client + ): def compression_settings(client): pool_options = client.options.pool_options return pool_options._compression_settings - uri = "mongodb://localhost:27017/?compressors=zlib" - client = self.simple_client(uri, connect=False) + client = await simple_client("mongodb://localhost:27017/?compressors=zlib", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=4" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + + client = await simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=4", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, 4) - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-1" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == 4 + + client = await simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-1", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017/?compressors=foobar" - client = self.simple_client(uri, connect=False) + assert opts.compressors == [] + assert opts.zlib_compression_level == -1 + + client = await simple_client("mongodb://localhost:27017/?compressors=foobar", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017/?compressors=foobar,zlib" - client = self.simple_client(uri, connect=False) + assert opts.compressors == [] + assert opts.zlib_compression_level == -1 + + client = await simple_client( + "mongodb://localhost:27017/?compressors=foobar,zlib", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 - # According to the connection string spec, unsupported values - # just raise a warning and are ignored. - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=10" - client = self.simple_client(uri, connect=False) + client = await simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=10", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-2" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = await simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-2", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 if not _have_snappy(): - uri = "mongodb://localhost:27017/?compressors=snappy" - client = self.simple_client(uri, connect=False) + client = await simple_client( + "mongodb://localhost:27017/?compressors=snappy", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) + assert opts.compressors == [] else: - uri = "mongodb://localhost:27017/?compressors=snappy" - client = self.simple_client(uri, connect=False) + client = await simple_client( + "mongodb://localhost:27017/?compressors=snappy", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["snappy"]) - uri = "mongodb://localhost:27017/?compressors=snappy,zlib" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["snappy"] + client = await simple_client( + "mongodb://localhost:27017/?compressors=snappy,zlib", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["snappy", "zlib"]) + assert opts.compressors == ["snappy", "zlib"] if not _have_zstd(): - uri = "mongodb://localhost:27017/?compressors=zstd" - client = self.simple_client(uri, connect=False) + client = await simple_client( + "mongodb://localhost:27017/?compressors=zstd", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) + assert opts.compressors == [] else: - uri = "mongodb://localhost:27017/?compressors=zstd" - client = self.simple_client(uri, connect=False) + client = await simple_client( + "mongodb://localhost:27017/?compressors=zstd", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zstd"]) - uri = "mongodb://localhost:27017/?compressors=zstd,zlib" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zstd"] + client = await simple_client( + "mongodb://localhost:27017/?compressors=zstd,zlib", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zstd", "zlib"]) + assert opts.compressors == ["zstd", "zlib"] - options = async_client_context.default_client_options + options = async_client_context_fixture.default_client_options if "compressors" in options and "zlib" in options["compressors"]: for level in range(-1, 10): - client = await self.async_single_client(zlibcompressionlevel=level) - # No error - await client.pymongo_test.test.find_one() + client = await async_single_client(zlibcompressionlevel=level) + await client.pymongo_test.test.find_one() # No error - @async_client_context.require_sync - async def test_reset_during_update_pool(self): - client = await self.async_rs_or_single_client(minPoolSize=10) + @pytest.mark.usefixtures("require_sync") + async def test_reset_during_update_pool(self, async_rs_or_single_client): + client = await async_rs_or_single_client(minPoolSize=10) await client.admin.command("ping") pool = await async_get_pool(client) generation = pool.gen.get_overall() @@ -1746,8 +1813,7 @@ def run(self): t = ResetPoolThread(pool) t.start() - # Ensure that update_pool completes without error even when the pool - # is reset concurrently. + # Ensure that update_pool completes without error even when the pool is reset concurrently. try: while True: for _ in range(10): @@ -1759,15 +1825,14 @@ def run(self): t.join() await client.admin.command("ping") - async def test_background_connections_do_not_hold_locks(self): + async def test_background_connections_do_not_hold_locks(self, async_rs_or_single_client): min_pool_size = 10 - client = await self.async_rs_or_single_client( + client = await async_rs_or_single_client( serverSelectionTimeoutMS=3000, minPoolSize=min_pool_size, connect=False ) - # Create a single connection in the pool. - await client.admin.command("ping") + await client.admin.command("ping") # Create a single connection in the pool - # Cause new connections stall for a few seconds. + # Cause new connections to stall for a few seconds. pool = await async_get_pool(client) original_connect = pool.connect @@ -1775,44 +1840,39 @@ async def stall_connect(*args, **kwargs): await asyncio.sleep(2) return await original_connect(*args, **kwargs) - pool.connect = stall_connect - # Un-patch Pool.connect to break the cyclic reference. - self.addCleanup(delattr, pool, "connect") - - # Wait for the background thread to start creating connections - await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") + try: + pool.connect = stall_connect + + await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") + # Assert that application operations do not block. + for _ in range(10): + start = time.monotonic() + await client.admin.command("ping") + total = time.monotonic() - start + assert total < 2 + finally: + delattr(pool, "connect") - # Assert that application operations do not block. - for _ in range(10): - start = time.monotonic() - await client.admin.command("ping") - total = time.monotonic() - start - # Each ping command should not take more than 2 seconds - self.assertLess(total, 2) - - @async_client_context.require_replica_set - async def test_direct_connection(self): - # direct_connection=True should result in Single topology. - client = await self.async_rs_or_single_client(directConnection=True) + @pytest.mark.usefixtures("require_replica_set") + async def test_direct_connection(self, async_rs_or_single_client): + client = await async_rs_or_single_client(directConnection=True) await client.admin.command("ping") - self.assertEqual(len(client.nodes), 1) - self.assertEqual(client._topology_settings.get_topology_type(), TOPOLOGY_TYPE.Single) + assert len(client.nodes) == 1 + assert client._topology_settings.get_topology_type() == TOPOLOGY_TYPE.Single - # direct_connection=False should result in RS topology. - client = await self.async_rs_or_single_client(directConnection=False) + client = await async_rs_or_single_client(directConnection=False) await client.admin.command("ping") - self.assertGreaterEqual(len(client.nodes), 1) - self.assertIn( - client._topology_settings.get_topology_type(), - [TOPOLOGY_TYPE.ReplicaSetNoPrimary, TOPOLOGY_TYPE.ReplicaSetWithPrimary], - ) + assert len(client.nodes) >= 1 + assert client._topology_settings.get_topology_type() in [ + TOPOLOGY_TYPE.ReplicaSetNoPrimary, + TOPOLOGY_TYPE.ReplicaSetWithPrimary, + ] - # directConnection=True, should error with multiple hosts as a list. - with self.assertRaises(ConfigurationError): + with pytest.raises(ConfigurationError): AsyncMongoClient(["host1", "host2"], directConnection=True) - @unittest.skipIf("PyPy" in sys.version, "PYTHON-2927 fails often on PyPy") - async def test_continuous_network_errors(self): + @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2927 fails often on PyPy") + async def test_continuous_network_errors(self, simple_client): def server_description_count(): i = 0 for obj in gc.get_objects(): @@ -1825,50 +1885,45 @@ def server_description_count(): gc.collect() with client_knobs(min_heartbeat_interval=0.003): - client = self.simple_client( + client = await simple_client( "invalid:27017", heartbeatFrequencyMS=3, serverSelectionTimeoutMS=150 ) initial_count = server_description_count() - with self.assertRaises(ServerSelectionTimeoutError): + with pytest.raises(ServerSelectionTimeoutError): await client.test.test.find_one() gc.collect() final_count = server_description_count() - # If a bug like PYTHON-2433 is reintroduced then too many - # ServerDescriptions will be kept alive and this test will fail: - # AssertionError: 19 != 46 within 15 delta (27 difference) - # On Python 3.11 we seem to get more of a delta. - self.assertAlmostEqual(initial_count, final_count, delta=20) - - @async_client_context.require_failCommand_fail_point - async def test_network_error_message(self): - client = await self.async_single_client(retryReads=False) + assert pytest.approx(initial_count, abs=20) == final_count + + @pytest.mark.usefixtures("require_failCommand_fail_point") + async def test_network_error_message(self, async_single_client): + client = await async_single_client(retryReads=False) await client.admin.command("ping") # connect async with self.fail_point( - {"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}} + client, + {"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}}, ): assert await client.address is not None expected = "{}:{}: ".format(*(await client.address)) - with self.assertRaisesRegex(AutoReconnect, expected): + with pytest.raises(AutoReconnect, match=expected): await client.pymongo_test.test.find_one({}) - @unittest.skipIf("PyPy" in sys.version, "PYTHON-2938 could fail on PyPy") - async def test_process_periodic_tasks(self): - client = await self.async_rs_or_single_client() + @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2938 could fail on PyPy") + async def test_process_periodic_tasks(self, async_rs_or_single_client): + client = await async_rs_or_single_client() coll = client.db.collection await coll.insert_many([{} for _ in range(5)]) cursor = coll.find(batch_size=2) await cursor.next() c_id = cursor.cursor_id - self.assertIsNotNone(c_id) + assert c_id is not None await client.close() - # Add cursor to kill cursors queue del cursor await async_wait_until( - lambda: client._kill_cursors_queue, - "waited for cursor to be added to queue", + lambda: client._kill_cursors_queue, "waited for cursor to be added to queue" ) await client._process_periodic_tasks() # This must not raise or print any exceptions - with self.assertRaises(InvalidOperation): + with pytest.raises(InvalidOperation): await coll.insert_many([{} for _ in range(5)]) async def test_service_name_from_kwargs(self): @@ -1877,82 +1932,79 @@ async def test_service_name_from_kwargs(self): srvServiceName="customname", connect=False, ) - self.assertEqual(client._topology_settings.srv_service_name, "customname") + assert client._topology_settings.srv_service_name == "customname" + client = AsyncMongoClient( - "mongodb+srv://user:password@test22.test.build.10gen.cc" - "/?srvServiceName=shouldbeoverriden", + "mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=shouldbeoverriden", srvServiceName="customname", connect=False, ) - self.assertEqual(client._topology_settings.srv_service_name, "customname") + assert client._topology_settings.srv_service_name == "customname" + client = AsyncMongoClient( "mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=customname", connect=False, ) - self.assertEqual(client._topology_settings.srv_service_name, "customname") + assert client._topology_settings.srv_service_name == "customname" + + async def test_srv_max_hosts_kwarg(self, simple_client): + client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/") + assert len(client.topology_description.server_descriptions()) > 1 - async def test_srv_max_hosts_kwarg(self): - client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/") - self.assertGreater(len(client.topology_description.server_descriptions()), 1) - client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1) - self.assertEqual(len(client.topology_description.server_descriptions()), 1) - client = self.simple_client( + client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1) + assert len(client.topology_description.server_descriptions()) == 1 + + client = await simple_client( "mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1", srvmaxhosts=2 ) - self.assertEqual(len(client.topology_description.server_descriptions()), 2) + assert len(client.topology_description.server_descriptions()) == 2 - @unittest.skipIf( - async_client_context.load_balancer or async_client_context.serverless, - "loadBalanced clients do not run SDAM", - ) - @unittest.skipIf(sys.platform == "win32", "Windows does not support SIGSTOP") - @async_client_context.require_sync - def test_sigstop_sigcont(self): + @pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support SIGSTOP") + @pytest.mark.usefixtures("require_sdam") + @pytest.mark.usefixtures("require_sync") + def test_sigstop_sigcont(self, async_client_context_fixture): test_dir = os.path.dirname(os.path.realpath(__file__)) script = os.path.join(test_dir, "sigstop_sigcont.py") - p = subprocess.Popen( - [sys.executable, script, async_client_context.uri], + with subprocess.Popen( + [sys.executable, script, async_client_context_fixture.uri], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - self.addCleanup(p.wait, timeout=1) - self.addCleanup(p.kill) - time.sleep(1) - # Stop the child, sleep for twice the streaming timeout - # (heartbeatFrequencyMS + connectTimeoutMS), and restart. - os.kill(p.pid, signal.SIGSTOP) - time.sleep(2) - os.kill(p.pid, signal.SIGCONT) - time.sleep(0.5) - # Tell the script to exit gracefully. - outs, _ = p.communicate(input=b"q\n", timeout=10) - self.assertTrue(outs) - log_output = outs.decode("utf-8") - self.assertIn("TEST STARTED", log_output) - self.assertIn("ServerHeartbeatStartedEvent", log_output) - self.assertIn("ServerHeartbeatSucceededEvent", log_output) - self.assertIn("TEST COMPLETED", log_output) - self.assertNotIn("ServerHeartbeatFailedEvent", log_output) - - async def _test_handshake(self, env_vars, expected_env): + ) as p: + time.sleep(1) + os.kill(p.pid, signal.SIGSTOP) + time.sleep(2) + os.kill(p.pid, signal.SIGCONT) + time.sleep(0.5) + outs, _ = p.communicate(input=b"q\n", timeout=10) + assert outs + log_output = outs.decode("utf-8") + assert "TEST STARTED" in log_output + assert "ServerHeartbeatStartedEvent" in log_output + assert "ServerHeartbeatSucceededEvent" in log_output + assert "TEST COMPLETED" in log_output + assert "ServerHeartbeatFailedEvent" not in log_output + + async def _test_handshake(self, env_vars, expected_env, async_rs_or_single_client): with patch.dict("os.environ", env_vars): metadata = copy.deepcopy(_METADATA) if has_c(): metadata["driver"]["name"] = "PyMongo|c|async" else: metadata["driver"]["name"] = "PyMongo|async" + if expected_env is not None: metadata["env"] = expected_env if "AWS_REGION" not in env_vars: os.environ["AWS_REGION"] = "" - client = await self.async_rs_or_single_client(serverSelectionTimeoutMS=10000) + + client = await async_rs_or_single_client(serverSelectionTimeoutMS=10000) await client.admin.command("ping") options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata - async def test_handshake_01_aws(self): + async def test_handshake_01_aws(self, async_rs_or_single_client): await self._test_handshake( { "AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", @@ -1960,12 +2012,18 @@ async def test_handshake_01_aws(self): "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", }, {"name": "aws.lambda", "region": "us-east-2", "memory_mb": 1024}, + async_rs_or_single_client, ) - async def test_handshake_02_azure(self): - await self._test_handshake({"FUNCTIONS_WORKER_RUNTIME": "python"}, {"name": "azure.func"}) + async def test_handshake_02_azure(self, async_rs_or_single_client): + await self._test_handshake( + {"FUNCTIONS_WORKER_RUNTIME": "python"}, + {"name": "azure.func"}, + async_rs_or_single_client, + ) - async def test_handshake_03_gcp(self): + async def test_handshake_03_gcp(self, async_rs_or_single_client): + # Regular case with environment variables. await self._test_handshake( { "K_SERVICE": "servicename", @@ -1974,7 +2032,9 @@ async def test_handshake_03_gcp(self): "FUNCTION_REGION": "us-central1", }, {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, + async_rs_or_single_client, ) + # Extra case for FUNCTION_NAME. await self._test_handshake( { @@ -1984,45 +2044,52 @@ async def test_handshake_03_gcp(self): "FUNCTION_REGION": "us-central1", }, {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, + async_rs_or_single_client, ) - async def test_handshake_04_vercel(self): + async def test_handshake_04_vercel(self, async_rs_or_single_client): await self._test_handshake( - {"VERCEL": "1", "VERCEL_REGION": "cdg1"}, {"name": "vercel", "region": "cdg1"} + {"VERCEL": "1", "VERCEL_REGION": "cdg1"}, + {"name": "vercel", "region": "cdg1"}, + async_rs_or_single_client, ) - async def test_handshake_05_multiple(self): + async def test_handshake_05_multiple(self, async_rs_or_single_client): await self._test_handshake( {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "FUNCTIONS_WORKER_RUNTIME": "python"}, None, + async_rs_or_single_client, ) - # Extra cases for other combos. + await self._test_handshake( {"FUNCTIONS_WORKER_RUNTIME": "python", "K_SERVICE": "servicename"}, None, + async_rs_or_single_client, + ) + + await self._test_handshake( + {"K_SERVICE": "servicename", "VERCEL": "1"}, None, async_rs_or_single_client ) - await self._test_handshake({"K_SERVICE": "servicename", "VERCEL": "1"}, None) - async def test_handshake_06_region_too_long(self): + async def test_handshake_06_region_too_long(self, async_rs_or_single_client): await self._test_handshake( {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_REGION": "a" * 512}, {"name": "aws.lambda"}, + async_rs_or_single_client, ) - async def test_handshake_07_memory_invalid_int(self): + async def test_handshake_07_memory_invalid_int(self, async_rs_or_single_client): await self._test_handshake( {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big"}, {"name": "aws.lambda"}, + async_rs_or_single_client, ) - async def test_handshake_08_invalid_aws_ec2(self): + async def test_handshake_08_invalid_aws_ec2(self, async_rs_or_single_client): # AWS_EXECUTION_ENV needs to start with "AWS_Lambda_". - await self._test_handshake( - {"AWS_EXECUTION_ENV": "EC2"}, - None, - ) + await self._test_handshake({"AWS_EXECUTION_ENV": "EC2"}, None, async_rs_or_single_client) - async def test_handshake_09_container_with_provider(self): + async def test_handshake_09_container_with_provider(self, async_rs_or_single_client): await self._test_handshake( { ENV_VAR_K8S: "1", @@ -2036,102 +2103,96 @@ async def test_handshake_09_container_with_provider(self): "region": "us-east-1", "memory_mb": 256, }, + async_rs_or_single_client, ) - def test_dict_hints(self): - self.db.t.find(hint={"x": 1}) + async def test_dict_hints(self, async_client_context_fixture): + async_client_context_fixture.client.db.t.find(hint={"x": 1}) - def test_dict_hints_sort(self): - result = self.db.t.find() + async def test_dict_hints_sort(self, async_client_context_fixture): + result = async_client_context_fixture.client.db.t.find() result.sort({"x": 1}) + async_client_context_fixture.client.db.t.find(sort={"x": 1}) - self.db.t.find(sort={"x": 1}) - - async def test_dict_hints_create_index(self): - await self.db.t.create_index({"x": pymongo.ASCENDING}) + async def test_dict_hints_create_index(self, async_client_context_fixture): + await async_client_context_fixture.client.db.t.create_index({"x": pymongo.ASCENDING}) - async def test_legacy_java_uuid_roundtrip(self): + async def test_legacy_java_uuid_roundtrip(self, async_client_context_fixture): data = BinaryData.java_data docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, JAVA_LEGACY)) - await async_client_context.client.pymongo_test.drop_collection("java_uuid") - db = async_client_context.client.pymongo_test + await async_client_context_fixture.client.pymongo_test.drop_collection("java_uuid") + db = async_client_context_fixture.client.pymongo_test coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=JAVA_LEGACY)) await coll.insert_many(docs) - self.assertEqual(5, await coll.count_documents({})) + assert await coll.count_documents({}) == 5 async for d in coll.find(): - self.assertEqual(d["newguid"], uuid.UUID(d["newguidstring"])) + assert d["newguid"] == uuid.UUID(d["newguidstring"]) coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) async for d in coll.find(): - self.assertNotEqual(d["newguid"], d["newguidstring"]) - await async_client_context.client.pymongo_test.drop_collection("java_uuid") + assert d["newguid"] != d["newguidstring"] + await async_client_context_fixture.client.pymongo_test.drop_collection("java_uuid") - async def test_legacy_csharp_uuid_roundtrip(self): + async def test_legacy_csharp_uuid_roundtrip(self, async_client_context_fixture): data = BinaryData.csharp_data docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, CSHARP_LEGACY)) - await async_client_context.client.pymongo_test.drop_collection("csharp_uuid") - db = async_client_context.client.pymongo_test + await async_client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") + db = async_client_context_fixture.client.pymongo_test coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=CSHARP_LEGACY)) await coll.insert_many(docs) - self.assertEqual(5, await coll.count_documents({})) + assert await coll.count_documents({}) == 5 async for d in coll.find(): - self.assertEqual(d["newguid"], uuid.UUID(d["newguidstring"])) + assert d["newguid"] == uuid.UUID(d["newguidstring"]) coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) async for d in coll.find(): - self.assertNotEqual(d["newguid"], d["newguidstring"]) - await async_client_context.client.pymongo_test.drop_collection("csharp_uuid") + assert d["newguid"] != d["newguidstring"] + await async_client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") - async def test_uri_to_uuid(self): + async def test_uri_to_uuid(self, async_single_client): uri = "mongodb://foo/?uuidrepresentation=csharpLegacy" - client = await self.async_single_client(uri, connect=False) - self.assertEqual(client.pymongo_test.test.codec_options.uuid_representation, CSHARP_LEGACY) + client = await async_single_client(uri, connect=False) + assert client.pymongo_test.test.codec_options.uuid_representation == CSHARP_LEGACY - async def test_uuid_queries(self): - db = async_client_context.client.pymongo_test + async def test_uuid_queries(self, async_client_context_fixture): + db = async_client_context_fixture.client.pymongo_test coll = db.test await coll.drop() uu = uuid.uuid4() await coll.insert_one({"uuid": Binary(uu.bytes, 3)}) - self.assertEqual(1, await coll.count_documents({})) + assert await coll.count_documents({}) == 1 - # Test regular UUID queries (using subtype 4). coll = db.get_collection( "test", CodecOptions(uuid_representation=UuidRepresentation.STANDARD) ) - self.assertEqual(0, await coll.count_documents({"uuid": uu})) + assert await coll.count_documents({"uuid": uu}) == 0 await coll.insert_one({"uuid": uu}) - self.assertEqual(2, await coll.count_documents({})) - docs = await coll.find({"uuid": uu}).to_list() - self.assertEqual(1, len(docs)) - self.assertEqual(uu, docs[0]["uuid"]) + assert await coll.count_documents({}) == 2 + docs = await coll.find({"uuid": uu}).to_list(length=1) + assert len(docs) == 1 + assert docs[0]["uuid"] == uu - # Test both. uu_legacy = Binary.from_uuid(uu, UuidRepresentation.PYTHON_LEGACY) predicate = {"uuid": {"$in": [uu, uu_legacy]}} - self.assertEqual(2, await coll.count_documents(predicate)) - docs = await coll.find(predicate).to_list() - self.assertEqual(2, len(docs)) + assert await coll.count_documents(predicate) == 2 + docs = await coll.find(predicate).to_list(length=2) + assert len(docs) == 2 await coll.drop() -class TestExhaustCursor(AsyncIntegrationTest): - """Test that clients properly handle errors from exhaust cursors.""" - - def setUp(self): - super().setUp() - if async_client_context.is_mongos: - raise SkipTest("mongos doesn't support exhaust, SERVER-2627") - - async def test_exhaust_query_server_error(self): +@pytest.mark.usefixtures("require_no_mongos") +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestExhaustCursor(AsyncPyMongoTestCasePyTest): + async def test_exhaust_query_server_error(self, async_rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid semaphore leaks. - client = await connected(await self.async_rs_or_single_client(maxPoolSize=1)) + client = await connected(await async_rs_or_single_client(maxPoolSize=1)) collection = client.pymongo_test.test pool = await async_get_pool(client) @@ -2143,23 +2204,22 @@ async def test_exhaust_query_server_error(self): SON([("$query", {}), ("$orderby", True)]), cursor_type=CursorType.EXHAUST ) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): await cursor.next() - self.assertFalse(conn.closed) + assert not conn.closed # The socket was checked in and the semaphore was decremented. - self.assertIn(conn, pool.conns) - self.assertEqual(0, pool.requests) + assert conn in pool.conns + assert pool.requests == 0 - async def test_exhaust_getmore_server_error(self): + async def test_exhaust_getmore_server_error(self, async_rs_or_single_client): # When doing a getmore on an exhaust cursor, the socket stays checked # out on success but it's checked in on error to avoid semaphore leaks. - client = await self.async_rs_or_single_client(maxPoolSize=1) + client = await async_rs_or_single_client(maxPoolSize=1) collection = client.pymongo_test.test await collection.drop() await collection.insert_many([{} for _ in range(200)]) - self.addAsyncCleanup(async_client_context.client.pymongo_test.test.drop) pool = await async_get_pool(client) pool._check_interval_seconds = None # Never check. @@ -2181,21 +2241,19 @@ async def receive_message(request_id): return message._OpReply.unpack(msg) conn.receive_message = receive_message - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): await cursor.to_list() # Unpatch the instance. del conn.receive_message # The socket is returned to the pool and it still works. - self.assertEqual(200, await collection.count_documents({})) - self.assertIn(conn, pool.conns) + assert 200 == await collection.count_documents({}) + assert conn in pool.conns - async def test_exhaust_query_network_error(self): + async def test_exhaust_query_network_error(self, async_rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid semaphore leaks. - client = await connected( - await self.async_rs_or_single_client(maxPoolSize=1, retryReads=False) - ) + client = await connected(await async_rs_or_single_client(maxPoolSize=1, retryReads=False)) collection = client.pymongo_test.test pool = await async_get_pool(client) pool._check_interval_seconds = None # Never check. @@ -2205,18 +2263,18 @@ async def test_exhaust_query_network_error(self): conn.conn.close() cursor = collection.find(cursor_type=CursorType.EXHAUST) - with self.assertRaises(ConnectionFailure): + with pytest.raises(ConnectionFailure): await cursor.next() - self.assertTrue(conn.closed) + assert conn.closed # The socket was closed and the semaphore was decremented. - self.assertNotIn(conn, pool.conns) - self.assertEqual(0, pool.requests) + assert conn not in pool.conns + assert 0 == pool.requests - async def test_exhaust_getmore_network_error(self): + async def test_exhaust_getmore_network_error(self, async_rs_or_single_client): # When doing a getmore on an exhaust cursor, the socket stays checked # out on success but it's checked in on error to avoid semaphore leaks. - client = await self.async_rs_or_single_client(maxPoolSize=1) + client = await async_rs_or_single_client(maxPoolSize=1) collection = client.pymongo_test.test await collection.drop() await collection.insert_many([{} for _ in range(200)]) # More than one batch. @@ -2233,39 +2291,39 @@ async def test_exhaust_getmore_network_error(self): conn.conn.close() # A getmore fails. - with self.assertRaises(ConnectionFailure): + with pytest.raises(ConnectionFailure): await cursor.to_list() - self.assertTrue(conn.closed) + assert conn.closed await async_wait_until( lambda: len(client._kill_cursors_queue) == 0, "waited for all killCursor requests to complete", ) # The socket was closed and the semaphore was decremented. - self.assertNotIn(conn, pool.conns) - self.assertEqual(0, pool.requests) + assert conn not in pool.conns + assert 0 == pool.requests - @async_client_context.require_sync - def test_gevent_task(self): + @pytest.mark.usefixtures("require_sync") + def test_gevent_task(self, async_client_context_fixture): if not gevent_monkey_patched(): - raise SkipTest("Must be running monkey patched by gevent") + pytest.skip("Must be running monkey patched by gevent") from gevent import spawn def poller(): while True: - async_client_context.client.pymongo_test.test.insert_one({}) + async_client_context_fixture.client.pymongo_test.test.insert_one({}) task = spawn(poller) task.kill() - self.assertTrue(task.dead) + assert task.dead - @async_client_context.require_sync - def test_gevent_timeout(self): + @pytest.mark.usefixtures("require_sync") + def test_gevent_timeout(self, async_rs_or_single_client): if not gevent_monkey_patched(): - raise SkipTest("Must be running monkey patched by gevent") + pytest.skip("Must be running monkey patched by gevent") from gevent import Timeout, spawn - client = self.async_rs_or_single_client(maxPoolSize=1) + client = async_rs_or_single_client(maxPoolSize=1) coll = client.pymongo_test.test coll.insert_one({}) @@ -2286,19 +2344,19 @@ def timeout_task(): tt = spawn(timeout_task) tt.join(15) ct.join(15) - self.assertTrue(tt.dead) - self.assertTrue(ct.dead) - self.assertIsNone(tt.get()) - self.assertIsNone(ct.get()) + assert tt.dead + assert ct.dead + assert tt.get() is None + assert ct.get() is None - @async_client_context.require_sync - def test_gevent_timeout_when_creating_connection(self): + @pytest.mark.usefixtures("require_sync") + def test_gevent_timeout_when_creating_connection(self, async_rs_or_single_client): if not gevent_monkey_patched(): - raise SkipTest("Must be running monkey patched by gevent") + pytest.skip("Must be running monkey patched by gevent") from gevent import Timeout, spawn - client = self.async_rs_or_single_client() - self.addCleanup(client.close) + client = async_rs_or_single_client() + coll = client.pymongo_test.test pool = async_get_pool(client) @@ -2321,23 +2379,26 @@ def timeout_task(): tt.join(10) # Assert that we got our active_sockets count back - self.assertEqual(pool.active_sockets, 0) + assert pool.active_sockets == 0 # Assert the greenlet is dead - self.assertTrue(tt.dead) + assert tt.dead # Assert that the Timeout was raised all the way to the try - self.assertTrue(tt.get()) + assert tt.get() # Unpatch the instance. del pool.connect -class TestClientLazyConnect(AsyncIntegrationTest): +@pytest.mark.usefixtures("require_sync") +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestClientLazyConnect: """Test concurrent operations on a lazily-connecting MongoClient.""" - def _get_client(self): - return self.async_rs_or_single_client(connect=False) + @pytest.fixture + def _get_client(self, async_rs_or_single_client): + return async_rs_or_single_client(connect=False) - @async_client_context.require_sync - def test_insert_one(self): + def test_insert_one(self, _get_client, async_client_context_fixture): def reset(collection): collection.drop() @@ -2345,12 +2406,11 @@ def insert_one(collection, _): collection.insert_one({}) def test(collection): - self.assertEqual(NTHREADS, collection.count_documents({})) + assert NTHREADS == collection.count_documents({}) - lazy_client_trial(reset, insert_one, test, self._get_client) + lazy_client_trial(reset, insert_one, test, _get_client, async_client_context_fixture) - @async_client_context.require_sync - def test_update_one(self): + def test_update_one(self, _get_client, async_client_context_fixture): def reset(collection): collection.drop() collection.insert_one({"i": 0}) @@ -2360,12 +2420,11 @@ def update_one(collection, _): collection.update_one({}, {"$inc": {"i": 1}}) def test(collection): - self.assertEqual(NTHREADS, collection.find_one()["i"]) + assert NTHREADS == collection.find_one()["i"] - lazy_client_trial(reset, update_one, test, self._get_client) + lazy_client_trial(reset, update_one, test, _get_client, async_client_context_fixture) - @async_client_context.require_sync - def test_delete_one(self): + def test_delete_one(self, _get_client, async_client_context_fixture): def reset(collection): collection.drop() collection.insert_many([{"i": i} for i in range(NTHREADS)]) @@ -2374,12 +2433,11 @@ def delete_one(collection, i): collection.delete_one({"i": i}) def test(collection): - self.assertEqual(0, collection.count_documents({})) + assert 0 == collection.count_documents({}) - lazy_client_trial(reset, delete_one, test, self._get_client) + lazy_client_trial(reset, delete_one, test, _get_client, async_client_context_fixture) - @async_client_context.require_sync - def test_find_one(self): + def test_find_one(self, _get_client, async_client_context_fixture): results: list = [] def reset(collection): @@ -2391,14 +2449,23 @@ def find_one(collection, _): results.append(collection.find_one()) def test(collection): - self.assertEqual(NTHREADS, len(results)) + assert NTHREADS == len(results) - lazy_client_trial(reset, find_one, test, self._get_client) + lazy_client_trial(reset, find_one, test, _get_client, async_client_context_fixture) -class TestMongoClientFailover(AsyncMockClientTest): - async def test_discover_primary(self): - c = await AsyncMockClient.get_async_mock_client( +@pytest.mark.usefixtures("require_no_load_balancer") +@pytest.mark.unit +class TestMongoClientFailover: + @pytest.fixture(scope="class", autouse=True) + def _client_knobs(self): + knobs = client_knobs(heartbeat_frequency=0.001, min_heartbeat_interval=0.001) + knobs.enable() + yield knobs + knobs.disable() + + async def test_discover_primary(self, async_mock_client): + c = await async_mock_client( standalones=[], members=["a:1", "b:2", "c:3"], mongoses=[], @@ -2406,11 +2473,10 @@ async def test_discover_primary(self): replicaSet="rs", heartbeatFrequencyMS=500, ) - self.addAsyncCleanup(c.close) await async_wait_until(lambda: len(c.nodes) == 3, "connect") - self.assertEqual(await c.address, ("a", 1)) + assert await c.address == ("a", 1) # Fail over. c.kill_host("a:1") c.mock_primary = "b:2" @@ -2420,11 +2486,11 @@ async def predicate(): await async_wait_until(predicate, "wait for server address to be updated") # a:1 not longer in nodes. - self.assertLess(len(c.nodes), 3) + assert len(c.nodes) < 3 - async def test_reconnect(self): + async def test_reconnect(self, async_mock_client): # Verify the node list isn't forgotten during a network failure. - c = await AsyncMockClient.get_async_mock_client( + c = await async_mock_client( standalones=[], members=["a:1", "b:2", "c:3"], mongoses=[], @@ -2433,7 +2499,6 @@ async def test_reconnect(self): retryReads=False, serverSelectionTimeoutMS=1000, ) - self.addAsyncCleanup(c.close) await async_wait_until(lambda: len(c.nodes) == 3, "connect") @@ -2442,10 +2507,10 @@ async def test_reconnect(self): c.kill_host("b:2") c.kill_host("c:3") - # AsyncMongoClient discovers it's alone. The first attempt raises either + # MongoClient discovers it's alone. The first attempt raises either # ServerSelectionTimeoutError or AutoReconnect (from # AsyncMockPool.get_socket). - with self.assertRaises(AutoReconnect): + with pytest.raises(AutoReconnect): await c.db.collection.find_one() # But it can reconnect. @@ -2453,14 +2518,14 @@ async def test_reconnect(self): await (await c._get_topology()).select_servers( writable_server_selector, _Op.TEST, server_selection_timeout=10 ) - self.assertEqual(await c.address, ("a", 1)) + assert await c.address == ("a", 1) - async def _test_network_error(self, operation_callback): + async def _test_network_error(self, async_mock_client, operation_callback): # Verify only the disconnected server is reset by a network failure. # Disable background refresh. with client_knobs(heartbeat_frequency=999999): - c = AsyncMockClient( + c = await async_mock_client( standalones=[], members=["a:1", "b:2"], mongoses=[], @@ -2471,8 +2536,6 @@ async def _test_network_error(self, operation_callback): serverSelectionTimeoutMS=1000, ) - self.addAsyncCleanup(c.close) - # Set host-specific information so we can test whether it is reset. c.set_wire_version_range("a:1", 2, MIN_SUPPORTED_WIRE_VERSION) c.set_wire_version_range("b:2", 2, MIN_SUPPORTED_WIRE_VERSION + 1) @@ -2481,62 +2544,63 @@ async def _test_network_error(self, operation_callback): c.kill_host("a:1") - # AsyncMongoClient is disconnected from the primary. This raises either + # MongoClient is disconnected from the primary. This raises either # ServerSelectionTimeoutError or AutoReconnect (from # MockPool.get_socket). - with self.assertRaises(AutoReconnect): + with pytest.raises(AutoReconnect): await operation_callback(c) # The primary's description is reset. server_a = (await c._get_topology()).get_server_by_address(("a", 1)) sd_a = server_a.description - self.assertEqual(SERVER_TYPE.Unknown, sd_a.server_type) - self.assertEqual(0, sd_a.min_wire_version) - self.assertEqual(0, sd_a.max_wire_version) + assert SERVER_TYPE.Unknown == sd_a.server_type + assert 0 == sd_a.min_wire_version + assert 0 == sd_a.max_wire_version # ...but not the secondary's. server_b = (await c._get_topology()).get_server_by_address(("b", 2)) sd_b = server_b.description - self.assertEqual(SERVER_TYPE.RSSecondary, sd_b.server_type) - self.assertEqual(2, sd_b.min_wire_version) - self.assertEqual(MIN_SUPPORTED_WIRE_VERSION + 1, sd_b.max_wire_version) + assert sd_b.server_type == SERVER_TYPE.RSSecondary + assert sd_b.min_wire_version == 2 + assert sd_b.max_wire_version == MIN_SUPPORTED_WIRE_VERSION + 1 - async def test_network_error_on_query(self): + async def test_network_error_on_query(self, async_mock_client): async def callback(client): return await client.db.collection.find_one() - await self._test_network_error(callback) + await self._test_network_error(async_mock_client, callback) - async def test_network_error_on_insert(self): + async def test_network_error_on_insert(self, async_mock_client): async def callback(client): return await client.db.collection.insert_one({}) - await self._test_network_error(callback) + await self._test_network_error(async_mock_client, callback) - async def test_network_error_on_update(self): + async def test_network_error_on_update(self, async_mock_client): async def callback(client): return await client.db.collection.update_one({}, {"$unset": "x"}) - await self._test_network_error(callback) + await self._test_network_error(async_mock_client, callback) - async def test_network_error_on_replace(self): + async def test_network_error_on_replace(self, async_mock_client): async def callback(client): return await client.db.collection.replace_one({}, {}) - await self._test_network_error(callback) + await self._test_network_error(async_mock_client, callback) - async def test_network_error_on_delete(self): + async def test_network_error_on_delete(self, async_mock_client): async def callback(client): return await client.db.collection.delete_many({}) - await self._test_network_error(callback) + await self._test_network_error(async_mock_client, callback) -class TestClientPool(AsyncMockClientTest): - @async_client_context.require_connection - async def test_rs_client_does_not_maintain_pool_to_arbiters(self): +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestClientPool: + async def test_rs_client_does_not_maintain_pool_to_arbiters(self, async_mock_client): listener = CMAPListener() - c = await AsyncMockClient.get_async_mock_client( + c = await async_mock_client( standalones=[], members=["a:1", "b:2", "c:3", "d:4"], mongoses=[], @@ -2547,27 +2611,21 @@ async def test_rs_client_does_not_maintain_pool_to_arbiters(self): minPoolSize=1, # minPoolSize event_listeners=[listener], ) - self.addAsyncCleanup(c.close) await async_wait_until(lambda: len(c.nodes) == 3, "connect") - self.assertEqual(await c.address, ("a", 1)) - self.assertEqual(await c.arbiters, {("c", 3)}) - # Assert that we create 2 and only 2 pooled connections. + assert await c.address == ("a", 1) + assert await c.arbiters == {("c", 3)} await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 2) - self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 2) - # Assert that we do not create connections to arbiters. + assert listener.event_count(monitoring.ConnectionCreatedEvent) == 2 arbiter = c._topology.get_server_by_address(("c", 3)) - self.assertFalse(arbiter.pool.conns) - # Assert that we do not create connections to unknown servers. + assert not arbiter.pool.conns arbiter = c._topology.get_server_by_address(("d", 4)) - self.assertFalse(arbiter.pool.conns) - # Arbiter pool is not marked ready. - self.assertEqual(listener.event_count(monitoring.PoolReadyEvent), 2) + assert not arbiter.pool.conns + assert listener.event_count(monitoring.PoolReadyEvent) == 2 - @async_client_context.require_connection - async def test_direct_client_maintains_pool_to_arbiter(self): + async def test_direct_client_maintains_pool_to_arbiter(self, async_mock_client): listener = CMAPListener() - c = await AsyncMockClient.get_async_mock_client( + c = await async_mock_client( standalones=[], members=["a:1", "b:2", "c:3"], mongoses=[], @@ -2577,18 +2635,11 @@ async def test_direct_client_maintains_pool_to_arbiter(self): minPoolSize=1, # minPoolSize event_listeners=[listener], ) - self.addAsyncCleanup(c.close) await async_wait_until(lambda: len(c.nodes) == 1, "connect") - self.assertEqual(await c.address, ("c", 3)) - # Assert that we create 1 pooled connection. + assert await c.address == ("c", 3) await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 1) - self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 1) + assert listener.event_count(monitoring.ConnectionCreatedEvent) == 1 arbiter = c._topology.get_server_by_address(("c", 3)) - self.assertEqual(len(arbiter.pool.conns), 1) - # Arbiter pool is marked ready. - self.assertEqual(listener.event_count(monitoring.PoolReadyEvent), 1) - - -if __name__ == "__main__": - unittest.main() + assert len(arbiter.pool.conns) == 1 + assert listener.event_count(monitoring.PoolReadyEvent) == 1 diff --git a/test/asynchronous/test_client_pytest.py b/test/asynchronous/test_client_pytest.py deleted file mode 100644 index 2c9e7d9a35..0000000000 --- a/test/asynchronous/test_client_pytest.py +++ /dev/null @@ -1,2515 +0,0 @@ -# Copyright 2013-present MongoDB, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test the mongo_client module.""" -from __future__ import annotations - -import _thread as thread -import asyncio -import contextlib -import copy -import datetime -import gc -import logging -import os -import re -import signal -import socket -import struct -import subprocess -import sys -import threading -import time -import uuid -from typing import Any, Iterable, Type, no_type_check -from unittest import mock -from unittest.mock import patch - -import pytest -import pytest_asyncio - -from bson.binary import CSHARP_LEGACY, JAVA_LEGACY, PYTHON_LEGACY, Binary, UuidRepresentation -from pymongo.operations import _Op - -sys.path[0:0] = [""] - -from test.asynchronous import ( - HAVE_IPADDRESS, - AsyncPyMongoTestCasePyTest, - SkipTest, - client_knobs, - connected, - db_pwd, - db_user, -) -from test.test_binary import BinaryData -from test.utils import ( - NTHREADS, - CMAPListener, - async_get_pool, - async_wait_until, - asyncAssertRaisesExactly, - delay, - gevent_monkey_patched, - is_greenthread_patched, - lazy_client_trial, - one, -) - -import bson -import pymongo -from bson import encode -from bson.codec_options import ( - CodecOptions, - DatetimeConversion, - TypeEncoder, - TypeRegistry, -) -from bson.son import SON -from bson.tz_util import utc -from pymongo import event_loggers, message, monitoring -from pymongo.asynchronous.command_cursor import AsyncCommandCursor -from pymongo.asynchronous.cursor import AsyncCursor, CursorType -from pymongo.asynchronous.database import AsyncDatabase -from pymongo.asynchronous.helpers import anext -from pymongo.asynchronous.mongo_client import AsyncMongoClient -from pymongo.asynchronous.pool import ( - AsyncConnection, -) -from pymongo.asynchronous.settings import TOPOLOGY_TYPE -from pymongo.asynchronous.topology import _ErrorContext -from pymongo.client_options import ClientOptions -from pymongo.common import _UUID_REPRESENTATIONS, CONNECT_TIMEOUT, MIN_SUPPORTED_WIRE_VERSION, has_c -from pymongo.compression_support import _have_snappy, _have_zstd -from pymongo.driver_info import DriverInfo -from pymongo.errors import ( - AutoReconnect, - ConfigurationError, - ConnectionFailure, - InvalidName, - InvalidOperation, - InvalidURI, - NetworkTimeout, - OperationFailure, - ServerSelectionTimeoutError, - WriteConcernError, -) -from pymongo.monitoring import ServerHeartbeatListener, ServerHeartbeatStartedEvent -from pymongo.pool_options import _MAX_METADATA_SIZE, _METADATA, ENV_VAR_K8S, PoolOptions -from pymongo.read_preferences import ReadPreference -from pymongo.server_description import ServerDescription -from pymongo.server_selectors import readable_server_selector, writable_server_selector -from pymongo.server_type import SERVER_TYPE -from pymongo.topology_description import TopologyDescription -from pymongo.write_concern import WriteConcern - -_IS_SYNC = False - -pytestmark = pytest.mark.asyncio(loop_scope="session") - - - -class TestAsyncClientUnitTest: - @pytest_asyncio.fixture(loop_scope="session") - async def async_client(self, async_rs_or_single_client) -> AsyncMongoClient: - client = await async_rs_or_single_client( - connect=False, serverSelectionTimeoutMS=100 - ) - yield client - await client.close() - - async def test_keyword_arg_defaults(self, simple_client): - client = await simple_client( - socketTimeoutMS=None, - connectTimeoutMS=20000, - waitQueueTimeoutMS=None, - replicaSet=None, - read_preference=ReadPreference.PRIMARY, - ssl=False, - tlsCertificateKeyFile=None, - tlsAllowInvalidCertificates=True, - tlsCAFile=None, - connect=False, - serverSelectionTimeoutMS=12000, - ) - - options = client.options - pool_opts = options.pool_options - assert pool_opts.socket_timeout is None - # socket.Socket.settimeout takes a float in seconds - assert 20.0 == pool_opts.connect_timeout - assert pool_opts.wait_queue_timeout is None - assert pool_opts._ssl_context is None - assert options.replica_set_name is None - assert client.read_preference == ReadPreference.PRIMARY - assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 - - async def test_connect_timeout(self, simple_client): - client = await simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) - pool_opts = client.options.pool_options - assert pool_opts.socket_timeout is None - assert pool_opts.connect_timeout is None - - client = await simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) - pool_opts = client.options.pool_options - assert pool_opts.socket_timeout is None - assert pool_opts.connect_timeout is None - - client = await simple_client( - "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False - ) - pool_opts = client.options.pool_options - assert pool_opts.socket_timeout is None - assert pool_opts.connect_timeout is None - - async def test_types(self): - with pytest.raises(TypeError): - AsyncMongoClient(1) - with pytest.raises(TypeError): - AsyncMongoClient(1.14) - with pytest.raises(TypeError): - AsyncMongoClient("localhost", "27017") - with pytest.raises(TypeError): - AsyncMongoClient("localhost", 1.14) - with pytest.raises(TypeError): - AsyncMongoClient("localhost", []) - - with pytest.raises(ConfigurationError): - AsyncMongoClient([]) - - async def test_max_pool_size_zero(self, simple_client): - await simple_client(maxPoolSize=0) - - async def test_uri_detection(self): - with pytest.raises(ConfigurationError): - AsyncMongoClient("/foo") - with pytest.raises(ConfigurationError): - AsyncMongoClient("://") - with pytest.raises(ConfigurationError): - AsyncMongoClient("foo/") - - - async def test_get_db(self, async_client): - def make_db(base, name): - return base[name] - - with pytest.raises(InvalidName): - make_db(async_client, "") - with pytest.raises(InvalidName): - make_db(async_client, "te$t") - with pytest.raises(InvalidName): - make_db(async_client, "te.t") - with pytest.raises(InvalidName): - make_db(async_client, "te\\t") - with pytest.raises(InvalidName): - make_db(async_client, "te/t") - with pytest.raises(InvalidName): - make_db(async_client, "te st") - # Type and equality assertions - assert isinstance(async_client.test, AsyncDatabase) - assert async_client.test == async_client["test"] - assert async_client.test == AsyncDatabase(async_client, "test") - - async def test_get_database(self, async_client): - codec_options = CodecOptions(tz_aware=True) - write_concern = WriteConcern(w=2, j=True) - db = async_client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) - assert db.name == "foo" - assert db.codec_options == codec_options - assert db.read_preference == ReadPreference.SECONDARY - assert db.write_concern == write_concern - - async def test_getattr(self, async_client): - assert isinstance(async_client["_does_not_exist"], AsyncDatabase) - - with pytest.raises(AttributeError) as context: - async_client.client._does_not_exist - - # Message should be: - # "AttributeError: AsyncMongoClient has no attribute '_does_not_exist'. To - # access the _does_not_exist database, use client['_does_not_exist']". - assert "has no attribute '_does_not_exist'" in str(context.value) - - - async def test_iteration(self, async_client): - if _IS_SYNC: - msg = "'AsyncMongoClient' object is not iterable" - else: - msg = "'AsyncMongoClient' object is not an async iterator" - - with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): - for _ in async_client: - break - - # Index fails - with pytest.raises(TypeError): - _ = async_client[0] - - # 'next' function fails - with pytest.raises(TypeError, match=msg): - _ = await anext(async_client) - - # 'next()' method fails - with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): - _ = await async_client.anext() - - # Do not implement typing.Iterable - assert not isinstance(async_client, Iterable) - - - async def test_get_default_database(self, async_rs_or_single_client, async_client_context): - c = await async_rs_or_single_client( - "mongodb://%s:%d/foo" - % (await async_client_context.host, await async_client_context.port), - connect=False, - ) - assert AsyncDatabase(c, "foo") == c.get_default_database() - # Test that default doesn't override the URI value. - assert AsyncDatabase(c, "foo") == c.get_default_database("bar") - codec_options = CodecOptions(tz_aware=True) - write_concern = WriteConcern(w=2, j=True) - db = c.get_default_database(None, codec_options, ReadPreference.SECONDARY, write_concern) - assert "foo" == db.name - assert codec_options == db.codec_options - assert ReadPreference.SECONDARY == db.read_preference - assert write_concern == db.write_concern - - c = await async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), - connect=False, - ) - assert AsyncDatabase(c, "foo") == c.get_default_database("foo") - - - async def test_get_default_database_error(self, async_rs_or_single_client, async_client_context): - # URI with no database. - c = await async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), - connect=False, - ) - with pytest.raises(ConfigurationError): - c.get_default_database() - - async def test_get_default_database_with_authsource(self, async_client_context, async_rs_or_single_client): - # Ensure we distinguish database name from authSource. - uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context.host, - await async_client_context.port, - ) - c = await async_rs_or_single_client(uri, connect=False) - assert (AsyncDatabase(c, "foo") == c.get_default_database()) - - async def test_get_database_default(self, async_client_context, async_rs_or_single_client): - c = await async_rs_or_single_client( - "mongodb://%s:%d/foo" - % (await async_client_context.host, await async_client_context.port), - connect=False, - ) - assert AsyncDatabase(c, "foo") == c.get_database() - - async def test_get_database_default_error(self, async_client_context, async_rs_or_single_client): - # URI with no database. - c = await async_rs_or_single_client( - "mongodb://%s:%d/" % (await async_client_context.host, await async_client_context.port), - connect=False, - ) - with pytest.raises(ConfigurationError): - c.get_database() - - async def test_get_database_default_with_authsource(self, async_client_context, async_rs_or_single_client): - # Ensure we distinguish database name from authSource. - uri = "mongodb://%s:%d/foo?authSource=src" % ( - await async_client_context.host, - await async_client_context.port, - ) - c = await async_rs_or_single_client(uri, connect=False) - assert AsyncDatabase(c, "foo") == c.get_database() - - async def test_primary_read_pref_with_tags(self, async_single_client): - # No tags allowed with "primary". - with pytest.raises(ConfigurationError): - async with await async_single_client("mongodb://host/?readpreferencetags=dc:east"): - pass - with pytest.raises(ConfigurationError): - async with await async_single_client( - "mongodb://host/?readpreference=primary&readpreferencetags=dc:east" - ): - pass - - async def test_read_preference(self, async_client_context, async_rs_or_single_client): - c = await async_rs_or_single_client( - "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode - ) - assert c.read_preference == ReadPreference.NEAREST - - async def test_metadata(self, simple_client): - metadata = copy.deepcopy(_METADATA) - if has_c(): - metadata["driver"]["name"] = "PyMongo|c|async" - else: - metadata["driver"]["name"] = "PyMongo|async" - metadata["application"] = {"name": "foobar"} - - client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") - options = client.options - assert options.pool_options.metadata == metadata - - client = await simple_client("foo", 27017, appname="foobar", connect=False) - options = client.options - assert options.pool_options.metadata == metadata - - # No error - await simple_client(appname="x" * 128) - with pytest.raises(ValueError): - await simple_client(appname="x" * 129) - - # Bad "driver" options. - with pytest.raises(TypeError): - DriverInfo("Foo", 1, "a") - with pytest.raises(TypeError): - DriverInfo(version="1", platform="a") - with pytest.raises(TypeError): - DriverInfo() - with pytest.raises(TypeError): - await simple_client(driver=1) - with pytest.raises(TypeError): - await simple_client(driver="abc") - with pytest.raises(TypeError): - await simple_client(driver=("Foo", "1", "a")) - - # Test appending to driver info. - if has_c(): - metadata["driver"]["name"] = "PyMongo|c|async|FooDriver" - else: - metadata["driver"]["name"] = "PyMongo|async|FooDriver" - metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) - - client = await simple_client( - "foo", - 27017, - appname="foobar", - driver=DriverInfo("FooDriver", "1.2.3", None), - connect=False, - ) - options = client.options - assert options.pool_options.metadata == metadata - - metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) - client = await simple_client( - "foo", - 27017, - appname="foobar", - driver=DriverInfo("FooDriver", "1.2.3", "FooPlatform"), - connect=False, - ) - options = client.options - assert options.pool_options.metadata == metadata - - # Test truncating driver info metadata. - client = await simple_client( - driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), - connect=False, - ) - options = client.options - assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE - - client = await simple_client( - driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), - connect=False, - ) - options = client.options - assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE - - - @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) - async def test_container_metadata(self, simple_client): - metadata = copy.deepcopy(_METADATA) - metadata["driver"]["name"] = "PyMongo|async" - metadata["env"] = {} - metadata["env"]["container"] = {"orchestrator": "kubernetes"} - - client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") - options = client.options - assert options.pool_options.metadata["env"] == metadata["env"] - - - async def test_kwargs_codec_options(self, simple_client): - class MyFloatType: - def __init__(self, x): - self.__x = x - - @property - def x(self): - return self.__x - - class MyFloatAsIntEncoder(TypeEncoder): - python_type = MyFloatType - - def transform_python(self, value): - return int(value) - - # Ensure codec options are passed in correctly - document_class: Type[SON] = SON - type_registry = TypeRegistry([MyFloatAsIntEncoder()]) - tz_aware = True - uuid_representation_label = "javaLegacy" - unicode_decode_error_handler = "ignore" - tzinfo = utc - c = await simple_client( - document_class=document_class, - type_registry=type_registry, - tz_aware=tz_aware, - uuidrepresentation=uuid_representation_label, - unicode_decode_error_handler=unicode_decode_error_handler, - tzinfo=tzinfo, - connect=False, - ) - assert c.codec_options.document_class == document_class - assert c.codec_options.type_registry == type_registry - assert c.codec_options.tz_aware == tz_aware - assert c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] - assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler - assert c.codec_options.tzinfo == tzinfo - - - async def test_uri_codec_options(self, async_client_context, simple_client): - uuid_representation_label = "javaLegacy" - unicode_decode_error_handler = "ignore" - datetime_conversion = "DATETIME_CLAMP" - uri = ( - "mongodb://%s:%d/foo?tz_aware=true&uuidrepresentation=" - "%s&unicode_decode_error_handler=%s" - "&datetime_conversion=%s" - % ( - await async_client_context.host, - await async_client_context.port, - uuid_representation_label, - unicode_decode_error_handler, - datetime_conversion, - ) - ) - c = await simple_client(uri, connect=False) - assert c.codec_options.tz_aware is True - assert c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] - assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler - assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] - # Change the passed datetime_conversion to a number and re-assert. - uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") - c = await simple_client(uri, connect=False) - assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] - - async def test_uri_option_precedence(self, simple_client): - # Ensure kwarg options override connection string options. - uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" - c = await simple_client( - uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" - ) - clopts = c.options - opts = clopts._options - assert opts["tls"] is False - assert clopts.replica_set_name == "newname" - assert clopts.read_preference == ReadPreference.SECONDARY_PREFERRED - - async def test_connection_timeout_ms_propagates_to_DNS_resolver(self, patch_resolver, simple_client): - base_uri = "mongodb+srv://test5.test.build.10gen.cc" - connectTimeoutMS = 5000 - expected_kw_value = 5.0 - uri_with_timeout = base_uri + "/?connectTimeoutMS=6000" - expected_uri_value = 6.0 - async def test_scenario(args, kwargs, expected_value): - patch_resolver.reset() - await simple_client(*args, **kwargs) - for _, kw in patch_resolver.call_list(): - assert pytest.approx(kw["lifetime"], rel=1e-6) == expected_value - - # No timeout specified. - await test_scenario((base_uri,), {}, CONNECT_TIMEOUT) - - # Timeout only specified in connection string. - await test_scenario((uri_with_timeout,), {}, expected_uri_value) - - # Timeout only specified in keyword arguments. - kwarg = {"connectTimeoutMS": connectTimeoutMS} - await test_scenario((base_uri,), kwarg, expected_kw_value) - - # Timeout specified in both kwargs and connection string. - await test_scenario((uri_with_timeout,), kwarg, expected_kw_value) - - - async def test_uri_security_options(self, simple_client): - # Ensure that we don't silently override security-related options. - with pytest.raises(InvalidURI): - await simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) - - # Matching SSL and TLS options should not cause errors. - c = await simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) - assert c.options._options["tls"] is False - - # Conflicting tlsInsecure options should raise an error. - with pytest.raises(InvalidURI): - await simple_client( - "mongodb://localhost/?tlsInsecure=true", - connect=False, - tlsAllowInvalidHostnames=True, - ) - - # Conflicting legacy tlsInsecure options should also raise an error. - with pytest.raises(InvalidURI): - await simple_client( - "mongodb://localhost/?tlsInsecure=true", - connect=False, - tlsAllowInvalidCertificates=False, - ) - - # Conflicting kwargs should raise InvalidURI - with pytest.raises(InvalidURI): - await simple_client(ssl=True, tls=False) - - async def test_event_listeners(self, simple_client): - c = await simple_client(event_listeners=[], connect=False) - assert c.options.event_listeners == [] - listeners = [ - event_loggers.CommandLogger(), - event_loggers.HeartbeatLogger(), - event_loggers.ServerLogger(), - event_loggers.TopologyLogger(), - event_loggers.ConnectionPoolLogger(), - ] - c = await simple_client(event_listeners=listeners, connect=False) - assert c.options.event_listeners == listeners - - async def test_client_options(self, simple_client): - c = await simple_client(connect=False) - assert isinstance(c.options, ClientOptions) - assert isinstance(c.options.pool_options, PoolOptions) - assert c.options.server_selection_timeout == 30 - assert c.options.pool_options.max_idle_time_seconds is None - assert isinstance(c.options.retry_writes, bool) - assert isinstance(c.options.retry_reads, bool) - - async def test_validate_suggestion(self): - """Validate kwargs in constructor.""" - for typo in ["auth", "Auth", "AUTH"]: - expected = ( - f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, " - f"authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." - ) - expected = re.escape(expected) - with pytest.raises(ConfigurationError, match=expected): - AsyncMongoClient(**{typo: "standard"}) # type: ignore[arg-type] - - async def test_detected_environment_logging(self, caplog): - normal_hosts = [ - "normal.host.com", - "host.cosmos.azure.com", - "host.docdb.amazonaws.com", - "host.docdb-elastic.amazonaws.com", - ] - srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] - multi_host = ( - "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" - ) - with caplog.at_level(logging.INFO, logger="pymongo"): - with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: - for host in normal_hosts: - AsyncMongoClient(host, connect=False) - for host in srv_hosts: - mock_get_hosts.return_value = [(host, 1)] - AsyncMongoClient(host, connect=False) - AsyncMongoClient(multi_host, connect=False) - logs = [record.getMessage() for record in caplog.records if record.name == "pymongo.client"] - assert len(logs) == 7 - - async def test_detected_environment_warning(self, caplog, simple_client): - normal_hosts = [ - "host.cosmos.azure.com", - "host.docdb.amazonaws.com", - "host.docdb-elastic.amazonaws.com", - ] - srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] - multi_host = ( - "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" - ) - with caplog.at_level(logging.WARN, logger="pymongo"): - with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: - with pytest.warns(UserWarning): - for host in normal_hosts: - await simple_client(host) - for host in srv_hosts: - mock_get_hosts.return_value = [(host, 1)] - await simple_client(host) - await simple_client(multi_host) - - -@pytest.mark.usefixtures("integration_test") -class TestAsyncClientIntegrationTest(AsyncPyMongoTestCasePyTest): - - async def test_multiple_uris(self): - with pytest.raises(ConfigurationError): - AsyncMongoClient( - host=[ - "mongodb+srv://cluster-a.abc12.mongodb.net", - "mongodb+srv://cluster-b.abc12.mongodb.net", - "mongodb+srv://cluster-c.abc12.mongodb.net", - ] - ) - - async def test_max_idle_time_reaper_default(self, async_rs_or_single_client): - with client_knobs(kill_cursor_frequency=0.1): - # Assert reaper doesn't remove connections when maxIdleTimeMS not set - client = await async_rs_or_single_client() - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - async with server._pool.checkout() as conn: - pass - assert 1 == len(server._pool.conns) - assert conn in server._pool.conns - - async def test_max_idle_time_reaper_removes_stale_minPoolSize(self, async_rs_or_single_client): - with client_knobs(kill_cursor_frequency=0.1): - # Assert reaper removes idle socket and replaces it with a new one - client = await async_rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1) - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - async with server._pool.checkout() as conn: - pass - # When the reaper runs at the same time as the get_socket, two - # connections could be created and checked into the pool. - assert len(server._pool.conns) >= 1 - await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") - await async_wait_until(lambda: len(server._pool.conns) >= 1, "replace stale socket") - - async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self, async_rs_or_single_client): - with client_knobs(kill_cursor_frequency=0.1): - # Assert reaper respects maxPoolSize when adding new connections. - client = await async_rs_or_single_client( - maxIdleTimeMS=500, minPoolSize=1, maxPoolSize=1 - ) - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - async with server._pool.checkout() as conn: - pass - # When the reaper runs at the same time as the get_socket, - # maxPoolSize=1 should prevent two connections from being created. - assert 1 == len(server._pool.conns) - await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") - await async_wait_until(lambda: len(server._pool.conns) == 1, "replace stale socket") - - async def test_max_idle_time_reaper_removes_stale(self, async_rs_or_single_client): - with client_knobs(kill_cursor_frequency=0.1): - # Assert that the reaper has removed the idle socket and NOT replaced it. - client = await async_rs_or_single_client(maxIdleTimeMS=500) - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - async with server._pool.checkout() as conn_one: - pass - # Assert that the pool does not close connections prematurely - await asyncio.sleep(0.300) - async with server._pool.checkout() as conn_two: - pass - assert conn_one is conn_two - await async_wait_until( - lambda: len(server._pool.conns) == 0, - "stale socket reaped and new one NOT added to the pool", - ) - - async def test_min_pool_size(self, async_rs_or_single_client): - with client_knobs(kill_cursor_frequency=0.1): - client = await async_rs_or_single_client() - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - assert len(server._pool.conns) == 0 - - # Assert that pool started up at minPoolSize - client = await async_rs_or_single_client(minPoolSize=10) - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - await async_wait_until( - lambda: len(server._pool.conns) == 10, - "pool initialized with 10 connections", - ) - # Assert that if a socket is closed, a new one takes its place. - async with server._pool.checkout() as conn: - conn.close_conn(None) - await async_wait_until( - lambda: len(server._pool.conns) == 10, - "a closed socket gets replaced from the pool", - ) - assert conn not in server._pool.conns - - async def test_max_idle_time_checkout(self, async_rs_or_single_client): - # Use high frequency to test _get_socket_no_auth. - with client_knobs(kill_cursor_frequency=99999999): - client = await async_rs_or_single_client(maxIdleTimeMS=500) - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - async with server._pool.checkout() as conn: - pass - assert len(server._pool.conns) == 1 - await asyncio.sleep(1) # Sleep so that the socket becomes stale. - - async with server._pool.checkout() as new_conn: - assert conn != new_conn - assert len(server._pool.conns) == 1 - assert conn not in server._pool.conns - assert new_conn in server._pool.conns - - # Test that connections are reused if maxIdleTimeMS is not set. - client = await async_rs_or_single_client() - server = await (await client._get_topology()).select_server( - readable_server_selector, _Op.TEST - ) - async with server._pool.checkout() as conn: - pass - assert len(server._pool.conns) == 1 - await asyncio.sleep(1) - async with server._pool.checkout() as new_conn: - assert conn == new_conn - assert len(server._pool.conns) == 1 - - async def test_constants(self, async_client_context, simple_client): - """This test uses AsyncMongoClient explicitly to make sure that host and - port are not overloaded. - """ - host, port = await async_client_context.host, await async_client_context.port - kwargs: dict = async_client_context.default_client_options.copy() - if async_client_context.auth_enabled: - kwargs["username"] = db_user - kwargs["password"] = db_pwd - - # Set bad defaults. - AsyncMongoClient.HOST = "somedomainthatdoesntexist.org" - AsyncMongoClient.PORT = 123456789 - with pytest.raises(AutoReconnect): - c = await simple_client(serverSelectionTimeoutMS=10, **kwargs) - await connected(c) - c = await simple_client(host, port, **kwargs) - # Override the defaults. No error. - await connected(c) - # Set good defaults. - AsyncMongoClient.HOST = host - AsyncMongoClient.PORT = port - # No error. - c = await simple_client(**kwargs) - await connected(c) - - async def test_init_disconnected(self, async_client_context, async_rs_or_single_client, simple_client): - host, port = await async_client_context.host, await async_client_context.port - c = await async_rs_or_single_client(connect=False) - # is_primary causes client to block until connected - assert isinstance(await c.is_primary, bool) - c = await async_rs_or_single_client(connect=False) - assert isinstance(await c.is_mongos, bool) - c = await async_rs_or_single_client(connect=False) - assert isinstance(c.options.pool_options.max_pool_size, int) - assert isinstance(c.nodes, frozenset) - - c = await async_rs_or_single_client(connect=False) - assert c.codec_options == CodecOptions() - c = await async_rs_or_single_client(connect=False) - assert not await c.primary - assert not await c.secondaries - c = await async_rs_or_single_client(connect=False) - assert isinstance(c.topology_description, TopologyDescription) - assert c.topology_description == c._topology._description - if async_client_context.is_rs: - # The primary's host and port are from the replica set config. - assert await c.address is not None - else: - assert await c.address == (host, port) - bad_host = "somedomainthatdoesntexist.org" - c = await simple_client(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10) - with pytest.raises(ConnectionFailure): - await c.pymongo_test.test.find_one() - - async def test_init_disconnected_with_auth(self, simple_client): - uri = "mongodb://user:pass@somedomainthatdoesntexist" - c = await simple_client(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10) - with pytest.raises(ConnectionFailure): - await c.pymongo_test.test.find_one() - - async def test_equality(self, async_client_context, async_rs_or_single_client, simple_client): - seed = "{}:{}".format(*list(async_client_context.client._topology_settings.seeds)[0]) - c = await async_rs_or_single_client(seed, connect=False) - assert async_client_context.client == c - # Explicitly test inequality - assert not async_client_context.client != c - - c = await async_rs_or_single_client("invalid.com", connect=False) - assert async_client_context.client != c - assert async_client_context.client != c - - c1 = await simple_client("a", connect=False) - c2 = await simple_client("b", connect=False) - - # Seeds differ: - assert c1 != c2 - - c1 = await simple_client(["a", "b", "c"], connect=False) - c2 = await simple_client(["c", "a", "b"], connect=False) - - # Same seeds but out of order still compares equal: - assert c1 == c2 - - async def test_hashable(self, async_client_context, async_rs_or_single_client): - seed = "{}:{}".format(*list(async_client_context.client._topology_settings.seeds)[0]) - c = await async_rs_or_single_client(seed, connect=False) - assert c in {async_client_context.client} - c = await async_rs_or_single_client("invalid.com", connect=False) - assert c not in {async_client_context.client} - - async def test_host_w_port(self, async_client_context): - with pytest.raises(ValueError): - host = await async_client_context.host - await connected( - AsyncMongoClient( - f"{host}:1234567", - connectTimeoutMS=1, - serverSelectionTimeoutMS=10, - ) - ) - - async def test_repr(self, simple_client): - # Used to test 'eval' below. - import bson - - client = AsyncMongoClient( # type: ignore[type-var] - "mongodb://localhost:27017,localhost:27018/?replicaSet=replset" - "&connectTimeoutMS=12345&w=1&wtimeoutms=100", - connect=False, - document_class=SON, - ) - the_repr = repr(client) - assert "AsyncMongoClient(host=" in the_repr - assert "document_class=bson.son.SON, tz_aware=False, connect=False, " in the_repr - assert "connecttimeoutms=12345" in the_repr - assert "replicaset='replset'" in the_repr - assert "w=1" in the_repr - assert "wtimeoutms=100" in the_repr - async with eval(the_repr) as client_two: - assert client_two == client - client = await simple_client( - "localhost:27017,localhost:27018", - replicaSet="replset", - connectTimeoutMS=12345, - socketTimeoutMS=None, - w=1, - wtimeoutms=100, - connect=False, - ) - the_repr = repr(client) - assert "AsyncMongoClient(host=" in the_repr - assert "document_class=dict, tz_aware=False, connect=False, " in the_repr - assert "connecttimeoutms=12345" in the_repr - assert "replicaset='replset'" in the_repr - assert "sockettimeoutms=None" in the_repr - assert "w=1" in the_repr - assert "wtimeoutms=100" in the_repr - async with eval(the_repr) as client_two: - assert client_two == client - - async def test_getters(self, async_client_context): - await async_wait_until( - lambda: async_client_context.nodes == async_client_context.client.nodes, "find all nodes" - ) - - async def test_list_databases(self, async_client_context, async_rs_or_single_client): - cmd_docs = (await async_client_context.client.admin.command("listDatabases"))["databases"] - cursor = await async_client_context.client.list_databases() - assert isinstance(cursor, AsyncCommandCursor) - helper_docs = await cursor.to_list() - assert len(helper_docs) > 0 - assert len(helper_docs) == len(cmd_docs) - # PYTHON-3529 Some fields may change between calls, just compare names. - for helper_doc, cmd_doc in zip(helper_docs, cmd_docs): - assert isinstance(helper_doc, dict) - assert helper_doc.keys() == cmd_doc.keys() - - client_doc = await async_rs_or_single_client(document_class=SON) - async for doc in await client_doc.list_databases(): - assert isinstance(doc, dict) - - await async_client_context.client.pymongo_test.test.insert_one({}) - cursor = await async_client_context.client.list_databases(filter={"name": "admin"}) - docs = await cursor.to_list() - assert len(docs) == 1 - assert docs[0]["name"] == "admin" - - cursor = await async_client_context.client.list_databases(nameOnly=True) - async for doc in cursor: - assert list(doc) == ["name"] - - async def test_list_database_names(self, async_client_context): - await async_client_context.client.pymongo_test.test.insert_one({"dummy": "object"}) - await async_client_context.client.pymongo_test_mike.test.insert_one({"dummy": "object"}) - cmd_docs = (await async_client_context.client.admin.command("listDatabases"))["databases"] - cmd_names = [doc["name"] for doc in cmd_docs] - - db_names = await async_client_context.client.list_database_names() - assert "pymongo_test" in db_names - assert "pymongo_test_mike" in db_names - assert db_names == cmd_names - - async def test_drop_database(self, async_client_context, async_rs_or_single_client): - with pytest.raises(TypeError): - await async_client_context.client.drop_database(5) # type: ignore[arg-type] - with pytest.raises(TypeError): - await async_client_context.client.drop_database(None) # type: ignore[arg-type] - - await async_client_context.client.pymongo_test.test.insert_one({"dummy": "object"}) - await async_client_context.client.pymongo_test2.test.insert_one({"dummy": "object"}) - dbs = await async_client_context.client.list_database_names() - assert "pymongo_test" in dbs - assert "pymongo_test2" in dbs - await async_client_context.client.drop_database("pymongo_test") - - if async_client_context.is_rs: - wc_client = await async_rs_or_single_client(w=len(async_client_context.nodes) + 1) - with pytest.raises(WriteConcernError): - await wc_client.drop_database("pymongo_test2") - - await async_client_context.client.drop_database(async_client_context.client.pymongo_test2) - dbs = await async_client_context.client.list_database_names() - assert "pymongo_test" not in dbs - assert "pymongo_test2" not in dbs - - async def test_close(self, async_rs_or_single_client): - test_client = await async_rs_or_single_client() - coll = test_client.pymongo_test.bar - await test_client.close() - with pytest.raises(InvalidOperation): - await coll.count_documents({}) - - async def test_close_kills_cursors(self, async_rs_or_single_client): - if sys.platform.startswith("java"): - # We can't figure out how to make this test reliable with Jython. - raise SkipTest("Can't test with Jython") - test_client = await async_rs_or_single_client() - # Kill any cursors possibly queued up by previous tests. - gc.collect() - await test_client._process_periodic_tasks() - - # Add some test data. - coll = test_client.pymongo_test.test_close_kills_cursors - docs_inserted = 1000 - await coll.insert_many([{"i": i} for i in range(docs_inserted)]) - - # Open a cursor and leave it open on the server. - cursor = coll.find().batch_size(10) - assert bool(await anext(cursor)) - assert cursor.retrieved < docs_inserted - - # Open a command cursor and leave it open on the server. - cursor = await coll.aggregate([], batchSize=10) - assert bool(await anext(cursor)) - del cursor - # Required for PyPy, Jython and other Python implementations that - # don't use reference counting garbage collection. - gc.collect() - - # Close the client and ensure the topology is closed. - assert test_client._topology._opened - await test_client.close() - assert not test_client._topology._opened - test_client = await async_rs_or_single_client() - # The killCursors task should not need to re-open the topology. - await test_client._process_periodic_tasks() - assert test_client._topology._opened - - async def test_close_stops_kill_cursors_thread(self, async_rs_client): - client = await async_rs_client() - await client.test.test.find_one() - assert not client._kill_cursors_executor._stopped - - # Closing the client should stop the thread. - await client.close() - assert client._kill_cursors_executor._stopped - - # Reusing the closed client should raise an InvalidOperation error. - with pytest.raises(InvalidOperation): - await client.admin.command("ping") - # Thread is still stopped. - assert client._kill_cursors_executor._stopped - - async def test_uri_connect_option(self, async_rs_client): - # Ensure that topology is not opened if connect=False. - client = await async_rs_client(connect=False) - assert not client._topology._opened - - # Ensure kill cursors thread has not been started. - if _IS_SYNC: - kc_thread = client._kill_cursors_executor._thread - assert not (kc_thread and kc_thread.is_alive()) - else: - kc_task = client._kill_cursors_executor._task - assert not (kc_task and not kc_task.done()) - # Using the client should open topology and start the thread. - await client.admin.command("ping") - assert client._topology._opened - if _IS_SYNC: - kc_thread = client._kill_cursors_executor._thread - assert kc_thread and kc_thread.is_alive() - else: - kc_task = client._kill_cursors_executor._task - assert kc_task and not kc_task.done() - - async def test_close_does_not_open_servers(self, async_rs_client): - client = await async_rs_client(connect=False) - topology = client._topology - assert topology._servers == {} - await client.close() - assert topology._servers == {} - - async def test_close_closes_sockets(self, async_rs_client): - client = await async_rs_client() - await client.test.test.find_one() - topology = client._topology - await client.close() - for server in topology._servers.values(): - assert not server._pool.conns - assert server._monitor._executor._stopped - assert server._monitor._rtt_monitor._executor._stopped - assert not server._monitor._pool.conns - assert not server._monitor._rtt_monitor._pool.conns - - async def test_bad_uri(self): - with pytest.raises(InvalidURI): - AsyncMongoClient("http://localhost") - - @pytest.mark.usefixtures("require_auth") - @pytest.mark.usefixtures("require_no_fips") - @pytest.mark.parametrize("remove_all_users_fixture", ["pymongo_test"], indirect=True) - @pytest.mark.parametrize("drop_user_fixture", [("admin", "admin")], indirect=True) - async def test_auth_from_uri(self, async_client_context, async_rs_or_single_client_noauth, remove_all_users_fixture, drop_user_fixture): - host, port = await async_client_context.host, await async_client_context.port - await async_client_context.create_user("admin", "admin", "pass") - - await async_client_context.create_user( - "pymongo_test", "user", "pass", roles=["userAdmin", "readWrite"] - ) - - with pytest.raises(OperationFailure): - await connected( - await async_rs_or_single_client_noauth("mongodb://a:b@%s:%d" % (host, port)) - ) - - # No error. - await connected( - await async_rs_or_single_client_noauth("mongodb://admin:pass@%s:%d" % (host, port)) - ) - - # Wrong database. - uri = "mongodb://admin:pass@%s:%d/pymongo_test" % (host, port) - with pytest.raises(OperationFailure): - await connected(await async_rs_or_single_client_noauth(uri)) - - # No error. - await connected( - await async_rs_or_single_client_noauth( - "mongodb://user:pass@%s:%d/pymongo_test" % (host, port) - ) - ) - - # Auth with lazy connection. - await ( - await async_rs_or_single_client_noauth( - "mongodb://user:pass@%s:%d/pymongo_test" % (host, port), connect=False - ) - ).pymongo_test.test.find_one() - - # Wrong password. - bad_client = await async_rs_or_single_client_noauth( - "mongodb://user:wrong@%s:%d/pymongo_test" % (host, port), connect=False - ) - - with pytest.raises(OperationFailure): - await bad_client.pymongo_test.test.find_one() - - @pytest.mark.usefixtures("require_auth") - @pytest.mark.parametrize("drop_user_fixture", [("admin", "ad min")], indirect=True) - async def test_username_and_password(self, async_client_context, async_rs_or_single_client_noauth, drop_user_fixture): - await async_client_context.create_user("admin", "ad min", "pa/ss") - - c = await async_rs_or_single_client_noauth(username="ad min", password="pa/ss") - - # Username and password aren't in strings that will likely be logged. - assert "ad min" not in repr(c) - assert "ad min" not in str(c) - assert "pa/ss" not in repr(c) - assert "pa/ss" not in str(c) - - # Auth succeeds. - await c.server_info() - - with pytest.raises(OperationFailure): - await ( - await async_rs_or_single_client_noauth(username="ad min", password="foo") - ).server_info() - - @pytest.mark.usefixtures("require_auth") - @pytest.mark.usefixtures("require_no_fips") - async def test_lazy_auth_raises_operation_failure(self, async_client_context, async_rs_or_single_client_noauth): - host = await async_client_context.host - lazy_client = await async_rs_or_single_client_noauth( - f"mongodb://user:wrong@{host}/pymongo_test", connect=False - ) - - await asyncAssertRaisesExactly(OperationFailure, lazy_client.test.collection.find_one) - - - @pytest.mark.usefixtures("require_no_tls") - async def test_unix_socket(self, async_client_context, async_rs_or_single_client, simple_client): - if not hasattr(socket, "AF_UNIX"): - pytest.skip("UNIX-sockets are not supported on this system") - - mongodb_socket = "/tmp/mongodb-%d.sock" % (await async_client_context.port,) - encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (await async_client_context.port,) - if not os.access(mongodb_socket, os.R_OK): - pytest.skip("Socket file is not accessible") - - uri = "mongodb://%s" % encoded_socket - # Confirm we can do operations via the socket. - client = await async_rs_or_single_client(uri) - await client.pymongo_test.test.insert_one({"dummy": "object"}) - dbs = await client.list_database_names() - assert "pymongo_test" in dbs - - assert mongodb_socket in repr(client) - - # Confirm it fails with a missing socket. - with pytest.raises(ConnectionFailure): - c = await simple_client( - "mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100 - ) - await connected(c) - - async def test_document_class(self, async_client_context, async_rs_or_single_client): - c = async_client_context.client - db = c.pymongo_test - await db.test.insert_one({"x": 1}) - - assert dict == c.codec_options.document_class - assert isinstance(await db.test.find_one(), dict) - assert not isinstance(await db.test.find_one(), SON) - - c = await async_rs_or_single_client(document_class=SON) - - db = c.pymongo_test - - assert SON == c.codec_options.document_class - assert isinstance(await db.test.find_one(), SON) - - - async def test_timeouts(self, async_rs_or_single_client): - client = await async_rs_or_single_client( - connectTimeoutMS=10500, - socketTimeoutMS=10500, - maxIdleTimeMS=10500, - serverSelectionTimeoutMS=10500, - ) - assert 10.5 == (await async_get_pool(client)).opts.connect_timeout - assert 10.5 == (await async_get_pool(client)).opts.socket_timeout - assert 10.5 == (await async_get_pool(client)).opts.max_idle_time_seconds - assert 10.5 == client.options.pool_options.max_idle_time_seconds - assert 10.5 == client.options.server_selection_timeout - - async def test_socket_timeout_ms_validation(self, async_rs_or_single_client): - c = await async_rs_or_single_client(socketTimeoutMS=10 * 1000) - assert 10 == (await async_get_pool(c)).opts.socket_timeout - - c = await connected(await async_rs_or_single_client(socketTimeoutMS=None)) - assert None == (await async_get_pool(c)).opts.socket_timeout - - c = await connected(await async_rs_or_single_client(socketTimeoutMS=0)) - assert None == (await async_get_pool(c)).opts.socket_timeout - - with pytest.raises(ValueError): - async with await async_rs_or_single_client(socketTimeoutMS=-1): - pass - - with pytest.raises(ValueError): - async with await async_rs_or_single_client(socketTimeoutMS=1e10): - pass - - with pytest.raises(ValueError): - async with await async_rs_or_single_client(socketTimeoutMS="foo"): - pass - - async def test_socket_timeout(self, async_client_context, async_rs_or_single_client): - no_timeout = async_client_context.client - timeout_sec = 1 - timeout = await async_rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) - - await no_timeout.pymongo_test.drop_collection("test") - await no_timeout.pymongo_test.test.insert_one({"x": 1}) - - # A $where clause that takes a second longer than the timeout - where_func = delay(timeout_sec + 1) - - async def get_x(db): - doc = await anext(db.test.find().where(where_func)) - return doc["x"] - - assert 1 == await get_x(no_timeout.pymongo_test) - with pytest.raises(NetworkTimeout): - await get_x(timeout.pymongo_test) - - async def test_server_selection_timeout(self): - client = AsyncMongoClient(serverSelectionTimeoutMS=100, connect=False) - pytest.approx(client.options.server_selection_timeout, 0.1) - await client.close() - - client = AsyncMongoClient(serverSelectionTimeoutMS=0, connect=False) - - pytest.approx(client.options.server_selection_timeout, 0) - - pytest.raises( - ValueError, AsyncMongoClient, serverSelectionTimeoutMS="foo", connect=False - ) - pytest.raises(ValueError, AsyncMongoClient, serverSelectionTimeoutMS=-1, connect=False) - pytest.raises( - ConfigurationError, AsyncMongoClient, serverSelectionTimeoutMS=None, connect=False - ) - await client.close() - - client = AsyncMongoClient( - "mongodb://localhost/?serverSelectionTimeoutMS=100", connect=False - ) - pytest.approx(client.options.server_selection_timeout, 0.1) - await client.close() - - client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=0", connect=False) - pytest.approx(client.options.server_selection_timeout, 0) - await client.close() - - # Test invalid timeout in URI ignored and set to default. - client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=-1", connect=False) - pytest.approx(client.options.server_selection_timeout, 30) - await client.close() - - client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=", connect=False) - pytest.approx(client.options.server_selection_timeout, 30) - - async def test_waitQueueTimeoutMS(self, async_rs_or_single_client): - client = await async_rs_or_single_client(waitQueueTimeoutMS=2000) - assert 2 == (await async_get_pool(client)).opts.wait_queue_timeout - - async def test_socketKeepAlive(self, async_client_context): - pool = await async_get_pool(async_client_context.client) - async with pool.checkout() as conn: - keepalive = conn.conn.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) - assert keepalive - - @no_type_check - async def test_tz_aware(self, async_client_context, async_rs_or_single_client): - pytest.raises(ValueError, AsyncMongoClient, tz_aware="foo") - - aware = await async_rs_or_single_client(tz_aware=True) - naive = async_client_context.client - await aware.pymongo_test.drop_collection("test") - - now = datetime.datetime.now(tz=datetime.timezone.utc) - await aware.pymongo_test.test.insert_one({"x": now}) - - assert None == (await naive.pymongo_test.test.find_one())["x"].tzinfo - assert utc == (await aware.pymongo_test.test.find_one())["x"].tzinfo - assert ( - (await aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None) == - (await naive.pymongo_test.test.find_one())["x"] - ) - - @pytest.mark.usefixtures("require_ipv6") - async def test_ipv6(self, async_client_context, async_rs_or_single_client_noauth): - if async_client_context.tls: - if not HAVE_IPADDRESS: - pytest.skip("Need the ipaddress module to test with SSL") - - if async_client_context.auth_enabled: - auth_str = f"{db_user}:{db_pwd}@" - else: - auth_str = "" - - uri = "mongodb://%s[::1]:%d" % (auth_str, await async_client_context.port) - if async_client_context.is_rs: - uri += "/?replicaSet=" + (async_client_context.replica_set_name or "") - - client = await async_rs_or_single_client_noauth(uri) - await client.pymongo_test.test.insert_one({"dummy": "object"}) - await client.pymongo_test_bernie.test.insert_one({"dummy": "object"}) - - dbs = await client.list_database_names() - assert "pymongo_test" in dbs - assert "pymongo_test_bernie" in dbs - - async def test_contextlib(self, async_rs_or_single_client): - client = await async_rs_or_single_client() - await client.pymongo_test.drop_collection("test") - await client.pymongo_test.test.insert_one({"foo": "bar"}) - - # The socket used for the previous commands has been returned to the - # pool - assert 1 == len((await async_get_pool(client)).conns) - - # contextlib async support was added in Python 3.10 - if _IS_SYNC or sys.version_info >= (3, 10): - async with contextlib.aclosing(client): - assert "bar" == (await client.pymongo_test.test.find_one())["foo"] - with pytest.raises(InvalidOperation): - await client.pymongo_test.test.find_one() - client = await async_rs_or_single_client() - async with client as client: - assert "bar" == (await client.pymongo_test.test.find_one())["foo"] - with pytest.raises(InvalidOperation): - await client.pymongo_test.test.find_one() - - @pytest.mark.usefixtures("require_sync") - def test_interrupt_signal(self, async_client_context): - if sys.platform.startswith("java"): - # We can't figure out how to raise an exception on a thread that's - # blocked on a socket, whether that's the main thread or a worker, - # without simply killing the whole thread in Jython. This suggests - # PYTHON-294 can't actually occur in Jython. - pytest.skip("Can't test interrupts in Jython") - if is_greenthread_patched(): - pytest.skip("Can't reliably test interrupts with green threads") - - # Test fix for PYTHON-294 -- make sure AsyncMongoClient closes its - # socket if it gets an interrupt while waiting to recv() from it. - db = async_client_context.client.pymongo_test - - # A $where clause which takes 1.5 sec to execute - where = delay(1.5) - - # Need exactly 1 document so find() will execute its $where clause once - db.drop_collection("foo") - db.foo.insert_one({"_id": 1}) - - old_signal_handler = None - try: - # Platform-specific hacks for raising a KeyboardInterrupt on the - # main thread while find() is in-progress: On Windows, SIGALRM is - # unavailable so we use a second thread. In our Evergreen setup on - # Linux, the thread technique causes an error in the test at - # conn.recv(): TypeError: 'int' object is not callable - # We don't know what causes this, so we hack around it. - - if sys.platform == "win32": - - def interrupter(): - # Raises KeyboardInterrupt in the main thread - time.sleep(0.25) - thread.interrupt_main() - - thread.start_new_thread(interrupter, ()) - else: - # Convert SIGALRM to SIGINT -- it's hard to schedule a SIGINT - # for one second in the future, but easy to schedule SIGALRM. - def sigalarm(num, frame): - raise KeyboardInterrupt - - old_signal_handler = signal.signal(signal.SIGALRM, sigalarm) - signal.alarm(1) - - raised = False - try: - # Will be interrupted by a KeyboardInterrupt. - next(db.foo.find({"$where": where})) # type: ignore[call-overload] - except KeyboardInterrupt: - raised = True - - assert raised, "Didn't raise expected KeyboardInterrupt" - - # Raises AssertionError due to PYTHON-294 -- Mongo's response to - # the previous find() is still waiting to be read on the socket, - # so the request id's don't match. - assert {"_id": 1} == next(db.foo.find()) # type: ignore[call-overload] - finally: - if old_signal_handler: - signal.signal(signal.SIGALRM, old_signal_handler) - - async def test_operation_failure(self, async_single_client): - # Ensure AsyncMongoClient doesn't close socket after it gets an error - # response to getLastError. PYTHON-395. We need a new client here - # to avoid race conditions caused by replica set failover or idle - # socket reaping. - client = await async_single_client() - await client.pymongo_test.test.find_one() - pool = await async_get_pool(client) - socket_count = len(pool.conns) - assert socket_count >= 1 - old_conn = next(iter(pool.conns)) - await client.pymongo_test.test.drop() - await client.pymongo_test.test.insert_one({"_id": "foo"}) - with pytest.raises(OperationFailure): - await client.pymongo_test.test.insert_one({"_id": "foo"}) - - assert socket_count == len(pool.conns) - new_conn = next(iter(pool.conns)) - assert old_conn == new_conn - - @pytest.mark.parametrize("drop_database_fixture", ["test_lazy_connect_w0"], indirect=True) - async def test_lazy_connect_w0(self, async_client_context, async_rs_or_single_client, drop_database_fixture): - # Ensure that connect-on-demand works when the first operation is - # an unacknowledged write. This exercises _writable_max_wire_version(). - - # Use a separate collection to avoid races where we're still - # completing an operation on a collection while the next test begins. - await async_client_context.client.drop_database("test_lazy_connect_w0") - - client = await async_rs_or_single_client(connect=False, w=0) - await client.test_lazy_connect_w0.test.insert_one({}) - - async def predicate(): - return await client.test_lazy_connect_w0.test.count_documents({}) == 1 - - await async_wait_until(predicate, "find one document") - - client = await async_rs_or_single_client(connect=False, w=0) - await client.test_lazy_connect_w0.test.update_one({}, {"$set": {"x": 1}}) - - async def predicate(): - return (await client.test_lazy_connect_w0.test.find_one()).get("x") == 1 - - await async_wait_until(predicate, "update one document") - - client = await async_rs_or_single_client(connect=False, w=0) - await client.test_lazy_connect_w0.test.delete_one({}) - - async def predicate(): - return await client.test_lazy_connect_w0.test.count_documents({}) == 0 - - await async_wait_until(predicate, "delete one document") - - - @pytest.mark.usefixtures("require_no_mongos") - async def test_exhaust_network_error(self, async_rs_or_single_client): - # When doing an exhaust query, the socket stays checked out on success - # but must be checked in on error to avoid semaphore leaks. - client = await async_rs_or_single_client(maxPoolSize=1, retryReads=False) - collection = client.pymongo_test.test - pool = await async_get_pool(client) - pool._check_interval_seconds = None # Never check. - - # Ensure a socket. - await connected(client) - - # Cause a network error. - conn = one(pool.conns) - conn.conn.close() - cursor = collection.find(cursor_type=CursorType.EXHAUST) - with pytest.raises(ConnectionFailure): - await anext(cursor) - - assert conn.closed - - # The semaphore was decremented despite the error. - assert 0 == pool.requests - - @pytest.mark.usefixtures("require_auth") - async def test_auth_network_error(self, async_rs_or_single_client): - # Make sure there's no semaphore leak if we get a network error - # when authenticating a new socket with cached credentials. - - # Get a client with one socket so we detect if it's leaked. - c = await connected( - await async_rs_or_single_client( - maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False - ) - ) - - # Cause a network error on the actual socket. - pool = await async_get_pool(c) - conn = one(pool.conns) - conn.conn.close() - - # AsyncConnection.authenticate logs, but gets a socket.error. Should be - # reraised as AutoReconnect. - with pytest.raises(AutoReconnect): - await c.test.collection.find_one() - - # No semaphore leak, the pool is allowed to make a new socket. - await c.test.collection.find_one() - - @pytest.mark.usefixtures("require_no_replica_set") - async def test_connect_to_standalone_using_replica_set_name(self, async_single_client): - client = await async_single_client(replicaSet="anything", serverSelectionTimeoutMS=100) - with pytest.raises(AutoReconnect): - await client.test.test.find_one() - - @pytest.mark.usefixtures("require_replica_set") - async def test_stale_getmore(self, async_rs_client): - # A cursor is created, but its member goes down and is removed from - # the topology before the getMore message is sent. Test that - # AsyncMongoClient._run_operation_with_response handles the error. - with pytest.raises(AutoReconnect): - client = await async_rs_client(connect=False, serverSelectionTimeoutMS=100) - await client._run_operation( - operation=message._GetMore( - "pymongo_test", - "collection", - 101, - 1234, - client.codec_options, - ReadPreference.PRIMARY, - None, - client, - None, - None, - False, - None, - ), - unpack_res=AsyncCursor(client.pymongo_test.collection)._unpack_response, - address=("not-a-member", 27017), - ) - - async def test_heartbeat_frequency_ms(self, async_client_context, async_single_client): - class HeartbeatStartedListener(ServerHeartbeatListener): - def __init__(self): - self.results = [] - - def started(self, event): - self.results.append(event) - - def succeeded(self, event): - pass - - def failed(self, event): - pass - - old_init = ServerHeartbeatStartedEvent.__init__ - heartbeat_times = [] - - def init(self, *args): - old_init(self, *args) - heartbeat_times.append(time.time()) - - try: - ServerHeartbeatStartedEvent.__init__ = init # type: ignore - listener = HeartbeatStartedListener() - uri = "mongodb://%s:%d/?heartbeatFrequencyMS=500" % ( - await async_client_context.host, - await async_client_context.port, - ) - await async_single_client(uri, event_listeners=[listener]) - await async_wait_until( - lambda: len(listener.results) >= 2, "record two ServerHeartbeatStartedEvents" - ) - - # Default heartbeatFrequencyMS is 10 sec. Check the interval was - # closer to 0.5 sec with heartbeatFrequencyMS configured. - pytest.approx(heartbeat_times[1] - heartbeat_times[0], 0.5, abs=2) - - finally: - ServerHeartbeatStartedEvent.__init__ = old_init # type: ignore - - async def test_small_heartbeat_frequency_ms(self): - uri = "mongodb://example/?heartbeatFrequencyMS=499" - with pytest.raises(ConfigurationError) as context: - AsyncMongoClient(uri) - - assert "heartbeatFrequencyMS" in str(context.value) - - - async def test_compression(self, async_client_context, simple_client, async_single_client): - def compression_settings(client): - pool_options = client.options.pool_options - return pool_options._compression_settings - - client = await simple_client("mongodb://localhost:27017/?compressors=zlib", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zlib"] - - client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=4", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zlib"] - assert opts.zlib_compression_level == 4 - - client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-1", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zlib"] - assert opts.zlib_compression_level == -1 - - client = await simple_client("mongodb://localhost:27017", connect=False) - opts = compression_settings(client) - assert opts.compressors == [] - assert opts.zlib_compression_level == -1 - - client = await simple_client("mongodb://localhost:27017/?compressors=foobar", connect=False) - opts = compression_settings(client) - assert opts.compressors == [] - assert opts.zlib_compression_level == -1 - - client = await simple_client("mongodb://localhost:27017/?compressors=foobar,zlib", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zlib"] - assert opts.zlib_compression_level == -1 - - client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=10", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zlib"] - assert opts.zlib_compression_level == -1 - - client = await simple_client("mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-2", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zlib"] - assert opts.zlib_compression_level == -1 - - if not _have_snappy(): - client = await simple_client("mongodb://localhost:27017/?compressors=snappy", connect=False) - opts = compression_settings(client) - assert opts.compressors == [] - else: - client = await simple_client("mongodb://localhost:27017/?compressors=snappy", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["snappy"] - client = await simple_client("mongodb://localhost:27017/?compressors=snappy,zlib", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["snappy", "zlib"] - - if not _have_zstd(): - client = await simple_client("mongodb://localhost:27017/?compressors=zstd", connect=False) - opts = compression_settings(client) - assert opts.compressors == [] - else: - client = await simple_client("mongodb://localhost:27017/?compressors=zstd", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zstd"] - client = await simple_client("mongodb://localhost:27017/?compressors=zstd,zlib", connect=False) - opts = compression_settings(client) - assert opts.compressors == ["zstd", "zlib"] - - options = async_client_context.default_client_options - if "compressors" in options and "zlib" in options["compressors"]: - for level in range(-1, 10): - client = await async_single_client(zlibcompressionlevel=level) - await client.pymongo_test.test.find_one() # No error - - @pytest.mark.usefixtures("require_sync") - async def test_reset_during_update_pool(self, async_rs_or_single_client): - client = await async_rs_or_single_client(minPoolSize=10) - await client.admin.command("ping") - pool = await async_get_pool(client) - generation = pool.gen.get_overall() - - # Continuously reset the pool. - class ResetPoolThread(threading.Thread): - def __init__(self, pool): - super().__init__() - self.running = True - self.pool = pool - - def stop(self): - self.running = False - - async def _run(self): - while self.running: - exc = AutoReconnect("mock pool error") - ctx = _ErrorContext(exc, 0, pool.gen.get_overall(), False, None) - await client._topology.handle_error(pool.address, ctx) - await asyncio.sleep(0.001) - - def run(self): - asyncio.run(self._run()) - - t = ResetPoolThread(pool) - t.start() - - # Ensure that update_pool completes without error even when the pool is reset concurrently. - try: - while True: - for _ in range(10): - await client._topology.update_pool() - if generation != pool.gen.get_overall(): - break - finally: - t.stop() - t.join() - await client.admin.command("ping") - - async def test_background_connections_do_not_hold_locks(self, async_rs_or_single_client): - min_pool_size = 10 - client = await async_rs_or_single_client(serverSelectionTimeoutMS=3000, minPoolSize=min_pool_size, - connect=False) - await client.admin.command("ping") # Create a single connection in the pool - - # Cause new connections to stall for a few seconds. - pool = await async_get_pool(client) - original_connect = pool.connect - - async def stall_connect(*args, **kwargs): - await asyncio.sleep(2) - return await original_connect(*args, **kwargs) - - try: - pool.connect = stall_connect - - await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") - # Assert that application operations do not block. - for _ in range(10): - start = time.monotonic() - await client.admin.command("ping") - total = time.monotonic() - start - assert total < 2 - finally: - delattr(pool, "connect") - - @pytest.mark.usefixtures("require_replica_set") - async def test_direct_connection(self, async_rs_or_single_client): - client = await async_rs_or_single_client(directConnection=True) - await client.admin.command("ping") - assert len(client.nodes) == 1 - assert client._topology_settings.get_topology_type() == TOPOLOGY_TYPE.Single - - client = await async_rs_or_single_client(directConnection=False) - await client.admin.command("ping") - assert len(client.nodes) >= 1 - assert client._topology_settings.get_topology_type() in [ - TOPOLOGY_TYPE.ReplicaSetNoPrimary, TOPOLOGY_TYPE.ReplicaSetWithPrimary - ] - - with pytest.raises(ConfigurationError): - AsyncMongoClient(["host1", "host2"], directConnection=True) - - @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2927 fails often on PyPy") - async def test_continuous_network_errors(self, simple_client): - def server_description_count(): - i = 0 - for obj in gc.get_objects(): - try: - if isinstance(obj, ServerDescription): - i += 1 - except ReferenceError: - pass - return i - - gc.collect() - with client_knobs(min_heartbeat_interval=0.003): - client = await simple_client("invalid:27017", heartbeatFrequencyMS=3, serverSelectionTimeoutMS=150) - initial_count = server_description_count() - with pytest.raises(ServerSelectionTimeoutError): - await client.test.test.find_one() - gc.collect() - final_count = server_description_count() - assert pytest.approx(initial_count, abs=20) == final_count - - @pytest.mark.usefixtures("require_failCommand_fail_point") - async def test_network_error_message(self, async_single_client): - client = await async_single_client(retryReads=False) - await client.admin.command("ping") # connect - async with self.fail_point(client, {"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}}): - assert await client.address is not None - expected = "{}:{}: ".format(*(await client.address)) - with pytest.raises(AutoReconnect, match=expected): - await client.pymongo_test.test.find_one({}) - - @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2938 could fail on PyPy") - async def test_process_periodic_tasks(self, async_rs_or_single_client): - client = await async_rs_or_single_client() - coll = client.db.collection - await coll.insert_many([{} for _ in range(5)]) - cursor = coll.find(batch_size=2) - await cursor.next() - c_id = cursor.cursor_id - assert c_id is not None - await client.close() - del cursor - await async_wait_until(lambda: client._kill_cursors_queue, "waited for cursor to be added to queue") - await client._process_periodic_tasks() # This must not raise or print any exceptions - with pytest.raises(InvalidOperation): - await coll.insert_many([{} for _ in range(5)]) - - async def test_service_name_from_kwargs(self): - client = AsyncMongoClient("mongodb+srv://user:password@test22.test.build.10gen.cc", srvServiceName="customname", - connect=False) - assert client._topology_settings.srv_service_name == "customname" - - client = AsyncMongoClient( - "mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=shouldbeoverriden", - srvServiceName="customname", connect=False) - assert client._topology_settings.srv_service_name == "customname" - - client = AsyncMongoClient("mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=customname", - connect=False) - assert client._topology_settings.srv_service_name == "customname" - - async def test_srv_max_hosts_kwarg(self, simple_client): - client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/") - assert len(client.topology_description.server_descriptions()) > 1 - - client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1) - assert len(client.topology_description.server_descriptions()) == 1 - - client = await simple_client("mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1", srvmaxhosts=2) - assert len(client.topology_description.server_descriptions()) == 2 - - @pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support SIGSTOP") - @pytest.mark.usefixtures("require_sdam") - @pytest.mark.usefixtures("require_sync") - def test_sigstop_sigcont(self, async_client_context): - test_dir = os.path.dirname(os.path.realpath(__file__)) - script = os.path.join(test_dir, "sigstop_sigcont.py") - with subprocess.Popen( - [sys.executable, script, async_client_context.uri], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as p: - time.sleep(1) - os.kill(p.pid, signal.SIGSTOP) - time.sleep(2) - os.kill(p.pid, signal.SIGCONT) - time.sleep(0.5) - outs, _ = p.communicate(input=b"q\n", timeout=10) - assert outs - log_output = outs.decode("utf-8") - assert "TEST STARTED" in log_output - assert "ServerHeartbeatStartedEvent" in log_output - assert "ServerHeartbeatSucceededEvent" in log_output - assert "TEST COMPLETED" in log_output - assert "ServerHeartbeatFailedEvent" not in log_output - - async def _test_handshake(self, env_vars, expected_env, async_rs_or_single_client): - with patch.dict("os.environ", env_vars): - metadata = copy.deepcopy(_METADATA) - if has_c(): - metadata["driver"]["name"] = "PyMongo|c|async" - else: - metadata["driver"]["name"] = "PyMongo|async" - - if expected_env is not None: - metadata["env"] = expected_env - - if "AWS_REGION" not in env_vars: - os.environ["AWS_REGION"] = "" - - client = await async_rs_or_single_client(serverSelectionTimeoutMS=10000) - await client.admin.command("ping") - options = client.options - assert options.pool_options.metadata == metadata - - async def test_handshake_01_aws(self, async_rs_or_single_client): - await self._test_handshake( - { - "AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", - "AWS_REGION": "us-east-2", - "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", - }, - {"name": "aws.lambda", "region": "us-east-2", "memory_mb": 1024}, - async_rs_or_single_client, - ) - - async def test_handshake_02_azure(self, async_rs_or_single_client): - await self._test_handshake( - {"FUNCTIONS_WORKER_RUNTIME": "python"}, {"name": "azure.func"}, async_rs_or_single_client - ) - - async def test_handshake_03_gcp(self, async_rs_or_single_client): - # Regular case with environment variables. - await self._test_handshake( - { - "K_SERVICE": "servicename", - "FUNCTION_MEMORY_MB": "1024", - "FUNCTION_TIMEOUT_SEC": "60", - "FUNCTION_REGION": "us-central1", - }, - {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, - async_rs_or_single_client, - ) - - # Extra case for FUNCTION_NAME. - await self._test_handshake( - { - "FUNCTION_NAME": "funcname", - "FUNCTION_MEMORY_MB": "1024", - "FUNCTION_TIMEOUT_SEC": "60", - "FUNCTION_REGION": "us-central1", - }, - {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, - async_rs_or_single_client, - ) - - async def test_handshake_04_vercel(self, async_rs_or_single_client): - await self._test_handshake( - {"VERCEL": "1", "VERCEL_REGION": "cdg1"}, {"name": "vercel", "region": "cdg1"}, async_rs_or_single_client - ) - - async def test_handshake_05_multiple(self, async_rs_or_single_client): - await self._test_handshake( - {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "FUNCTIONS_WORKER_RUNTIME": "python"}, - None, - async_rs_or_single_client, - ) - - await self._test_handshake( - {"FUNCTIONS_WORKER_RUNTIME": "python", "K_SERVICE": "servicename"}, None, async_rs_or_single_client - ) - - await self._test_handshake({"K_SERVICE": "servicename", "VERCEL": "1"}, None, async_rs_or_single_client) - - async def test_handshake_06_region_too_long(self, async_rs_or_single_client): - await self._test_handshake( - {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_REGION": "a" * 512}, - {"name": "aws.lambda"}, - async_rs_or_single_client, - ) - - async def test_handshake_07_memory_invalid_int(self, async_rs_or_single_client): - await self._test_handshake( - {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big"}, - {"name": "aws.lambda"}, - async_rs_or_single_client, - ) - - async def test_handshake_08_invalid_aws_ec2(self, async_rs_or_single_client): - # AWS_EXECUTION_ENV needs to start with "AWS_Lambda_". - await self._test_handshake({"AWS_EXECUTION_ENV": "EC2"}, None, async_rs_or_single_client) - - async def test_handshake_09_container_with_provider(self, async_rs_or_single_client): - await self._test_handshake( - { - ENV_VAR_K8S: "1", - "AWS_LAMBDA_RUNTIME_API": "1", - "AWS_REGION": "us-east-1", - "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "256", - }, - { - "container": {"orchestrator": "kubernetes"}, - "name": "aws.lambda", - "region": "us-east-1", - "memory_mb": 256, - }, - async_rs_or_single_client, - ) - - async def test_dict_hints(self, async_client_context): - async_client_context.client.db.t.find(hint={"x": 1}) - - async def test_dict_hints_sort(self, async_client_context): - result = async_client_context.client.db.t.find() - result.sort({"x": 1}) - async_client_context.client.db.t.find(sort={"x": 1}) - - async def test_dict_hints_create_index(self, async_client_context): - await async_client_context.client.db.t.create_index({"x": pymongo.ASCENDING}) - - async def test_legacy_java_uuid_roundtrip(self, async_client_context): - data = BinaryData.java_data - docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, JAVA_LEGACY)) - - await async_client_context.client.pymongo_test.drop_collection("java_uuid") - db = async_client_context.client.pymongo_test - coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=JAVA_LEGACY)) - - await coll.insert_many(docs) - assert await coll.count_documents({}) == 5 - async for d in coll.find(): - assert d["newguid"] == uuid.UUID(d["newguidstring"]) - - coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) - async for d in coll.find(): - assert d["newguid"] != d["newguidstring"] - await async_client_context.client.pymongo_test.drop_collection("java_uuid") - - async def test_legacy_csharp_uuid_roundtrip(self, async_client_context): - data = BinaryData.csharp_data - docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, CSHARP_LEGACY)) - - await async_client_context.client.pymongo_test.drop_collection("csharp_uuid") - db = async_client_context.client.pymongo_test - coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=CSHARP_LEGACY)) - - await coll.insert_many(docs) - assert await coll.count_documents({}) == 5 - async for d in coll.find(): - assert d["newguid"] == uuid.UUID(d["newguidstring"]) - - coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) - async for d in coll.find(): - assert d["newguid"] != d["newguidstring"] - await async_client_context.client.pymongo_test.drop_collection("csharp_uuid") - - async def test_uri_to_uuid(self, async_single_client): - uri = "mongodb://foo/?uuidrepresentation=csharpLegacy" - client = await async_single_client(uri, connect=False) - assert client.pymongo_test.test.codec_options.uuid_representation == CSHARP_LEGACY - - async def test_uuid_queries(self, async_client_context): - db = async_client_context.client.pymongo_test - coll = db.test - await coll.drop() - - uu = uuid.uuid4() - await coll.insert_one({"uuid": Binary(uu.bytes, 3)}) - assert await coll.count_documents({}) == 1 - - coll = db.get_collection("test", CodecOptions(uuid_representation=UuidRepresentation.STANDARD)) - assert await coll.count_documents({"uuid": uu}) == 0 - await coll.insert_one({"uuid": uu}) - assert await coll.count_documents({}) == 2 - docs = await coll.find({"uuid": uu}).to_list(length=1) - assert len(docs) == 1 - assert docs[0]["uuid"] == uu - - uu_legacy = Binary.from_uuid(uu, UuidRepresentation.PYTHON_LEGACY) - predicate = {"uuid": {"$in": [uu, uu_legacy]}} - assert await coll.count_documents(predicate) == 2 - docs = await coll.find(predicate).to_list(length=2) - assert len(docs) == 2 - await coll.drop() - -@pytest.mark.usefixtures("require_no_mongos") -@pytest.mark.usefixtures("integration_test") -class TestAsyncExhaustCursor(AsyncPyMongoTestCasePyTest): - async def test_exhaust_query_server_error(self, async_rs_or_single_client): - # When doing an exhaust query, the socket stays checked out on success - # but must be checked in on error to avoid semaphore leaks. - client = await connected(await async_rs_or_single_client(maxPoolSize=1)) - - collection = client.pymongo_test.test - pool = await async_get_pool(client) - conn = one(pool.conns) - - # This will cause OperationFailure in all mongo versions since - # the value for $orderby must be a document. - cursor = collection.find( - SON([("$query", {}), ("$orderby", True)]), cursor_type=CursorType.EXHAUST - ) - - with pytest.raises(OperationFailure): - await cursor.next() - assert not conn.closed - - # The socket was checked in and the semaphore was decremented. - assert conn in pool.conns - assert pool.requests == 0 - - async def test_exhaust_getmore_server_error(self, async_rs_or_single_client): - # When doing a getmore on an exhaust cursor, the socket stays checked - # out on success but it's checked in on error to avoid semaphore leaks. - client = await async_rs_or_single_client(maxPoolSize=1) - collection = client.pymongo_test.test - await collection.drop() - - await collection.insert_many([{} for _ in range(200)]) - - pool = await async_get_pool(client) - pool._check_interval_seconds = None # Never check. - conn = one(pool.conns) - - cursor = collection.find(cursor_type=CursorType.EXHAUST) - - # Initial query succeeds. - await cursor.next() - - # Cause a server error on getmore. - async def receive_message(request_id): - # Discard the actual server response. - await AsyncConnection.receive_message(conn, request_id) - - # responseFlags bit 1 is QueryFailure. - msg = struct.pack(" Callable[..., MongoClient]: + """Make a direct connection. Don't authenticate.""" + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = _async_mongo_client( + client_context_fixture, h, p, authenticate=False, directConnection=True, **kwargs + ) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def single_client(client_context_fixture) -> Callable[..., MongoClient]: + """Make a direct connection, and authenticate if necessary.""" + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = _async_mongo_client(client_context_fixture, h, p, directConnection=True, **kwargs) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def rs_client_noauth(client_context_fixture) -> Callable[..., MongoClient]: + """Connect to the replica set. Don't authenticate.""" + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = _async_mongo_client(client_context_fixture, h, p, authenticate=False, **kwargs) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def rs_client(client_context_fixture) -> Callable[..., MongoClient]: + """Connect to the replica set and authenticate if necessary.""" + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = _async_mongo_client(client_context_fixture, h, p, **kwargs) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def rs_or_single_client_noauth(client_context_fixture) -> Callable[..., MongoClient]: + """Connect to the replica set if there is one, otherwise the standalone. + + Like rs_or_single_client, but does not authenticate. + """ + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = _async_mongo_client(client_context_fixture, h, p, authenticate=False, **kwargs) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def rs_or_single_client(client_context_fixture) -> Callable[..., MongoClient]: + """Connect to the replica set if there is one, otherwise the standalone. + + Authenticates if necessary. + """ + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + client = _async_mongo_client(client_context_fixture, h, p, **kwargs) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def simple_client() -> Callable[..., MongoClient]: + clients = [] + + def _make_client(h: Any = None, p: Any = None, **kwargs: Any): + if not h and not p: + client = MongoClient(**kwargs) + else: + client = MongoClient(h, p, **kwargs) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture(scope="function") +def patch_resolver(): + from pymongo.srv_resolver import _resolve + + patched_resolver = FunctionCallRecorder(_resolve) + pymongo.srv_resolver._resolve = patched_resolver + yield patched_resolver + pymongo.srv_resolver._resolve = _resolve + + +@pytest.fixture() +def mock_client(): + clients = [] + + def _make_client( + standalones, + members, + mongoses, + hello_hosts=None, + arbiters=None, + down_hosts=None, + *args, + **kwargs, + ): + client = MockClient.get_mock_client( + standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs + ) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() + + +@pytest.fixture() +def remove_all_users_fixture(client_context_fixture, request): + db_name = request.param + yield + client_context_fixture.client[db_name].command( + "dropAllUsersFromDatabase", 1, writeConcern={"w": client_context_fixture.w} + ) + + +@pytest.fixture() +def drop_user_fixture(client_context_fixture, request): + db, user = request.param + yield + client_context_fixture.drop_user(db, user) + + +@pytest.fixture() +def drop_database_fixture(client_context_fixture, request): + db = request.param + yield + client_context_fixture.client.drop_database(db) + + pytest_collection_modifyitems = pytest_conf.pytest_collection_modifyitems diff --git a/test/pymongo_mocks.py b/test/pymongo_mocks.py index 7662dc9682..243d9eb98d 100644 --- a/test/pymongo_mocks.py +++ b/test/pymongo_mocks.py @@ -165,7 +165,8 @@ def get_mock_client( standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs ) - c._connect() + if "connect" not in kwargs or "connect" in kwargs and kwargs["connect"]: + c._connect() return c def kill_host(self, host): diff --git a/test/test_client.py b/test/test_client.py index 2a33077f5f..d2f2c94cbe 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -17,7 +17,6 @@ import _thread as thread import asyncio -import base64 import contextlib import copy import datetime @@ -46,24 +45,18 @@ from test import ( HAVE_IPADDRESS, - IntegrationTest, - MockClientTest, + PyMongoTestCasePyTest, SkipTest, - UnitTest, - client_context, client_knobs, connected, db_pwd, db_user, - remove_all_users, - unittest, ) -from test.pymongo_mocks import MockClient from test.test_binary import BinaryData from test.utils import ( NTHREADS, CMAPListener, - FunctionCallRecorder, + _default_pytest_mark, assertRaisesExactly, delay, get_pool, @@ -124,20 +117,19 @@ _IS_SYNC = True -class ClientUnitTest(UnitTest): - """MongoClient tests that don't require a server.""" +pytestmark = _default_pytest_mark(_IS_SYNC) - client: MongoClient - def setUp(self) -> None: - self.client = self.rs_or_single_client(connect=False, serverSelectionTimeoutMS=100) - - @pytest.fixture(autouse=True) - def inject_fixtures(self, caplog): - self._caplog = caplog +@pytest.mark.unit +class TestClientUnitTest: + @pytest.fixture() + def client(self, rs_or_single_client) -> MongoClient: + client = rs_or_single_client(connect=False, serverSelectionTimeoutMS=100) + yield client + client.close() - def test_keyword_arg_defaults(self): - client = self.simple_client( + def test_keyword_arg_defaults(self, simple_client): + client = simple_client( socketTimeoutMS=None, connectTimeoutMS=20000, waitQueueTimeoutMS=None, @@ -153,216 +145,252 @@ def test_keyword_arg_defaults(self): options = client.options pool_opts = options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) + assert pool_opts.socket_timeout is None # socket.Socket.settimeout takes a float in seconds - self.assertEqual(20.0, pool_opts.connect_timeout) - self.assertEqual(None, pool_opts.wait_queue_timeout) - self.assertEqual(None, pool_opts._ssl_context) - self.assertEqual(None, options.replica_set_name) - self.assertEqual(ReadPreference.PRIMARY, client.read_preference) - self.assertAlmostEqual(12, client.options.server_selection_timeout) - - def test_connect_timeout(self): - client = self.simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) + assert 20.0 == pool_opts.connect_timeout + assert pool_opts.wait_queue_timeout is None + assert pool_opts._ssl_context is None + assert options.replica_set_name is None + assert client.read_preference == ReadPreference.PRIMARY + assert pytest.approx(client.options.server_selection_timeout, rel=1e-9) == 12 + + def test_connect_timeout(self, simple_client): + client = simple_client(connect=False, connectTimeoutMS=None, socketTimeoutMS=None) pool_opts = client.options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) - self.assertEqual(None, pool_opts.connect_timeout) + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None - client = self.simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) + client = simple_client(connect=False, connectTimeoutMS=0, socketTimeoutMS=0) pool_opts = client.options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) - self.assertEqual(None, pool_opts.connect_timeout) + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None - client = self.simple_client( + client = simple_client( "mongodb://localhost/?connectTimeoutMS=0&socketTimeoutMS=0", connect=False ) pool_opts = client.options.pool_options - self.assertEqual(None, pool_opts.socket_timeout) - self.assertEqual(None, pool_opts.connect_timeout) + assert pool_opts.socket_timeout is None + assert pool_opts.connect_timeout is None def test_types(self): - self.assertRaises(TypeError, MongoClient, 1) - self.assertRaises(TypeError, MongoClient, 1.14) - self.assertRaises(TypeError, MongoClient, "localhost", "27017") - self.assertRaises(TypeError, MongoClient, "localhost", 1.14) - self.assertRaises(TypeError, MongoClient, "localhost", []) - - self.assertRaises(ConfigurationError, MongoClient, []) - - def test_max_pool_size_zero(self): - self.simple_client(maxPoolSize=0) + with pytest.raises(TypeError): + MongoClient(1) + with pytest.raises(TypeError): + MongoClient(1.14) + with pytest.raises(TypeError): + MongoClient("localhost", "27017") + with pytest.raises(TypeError): + MongoClient("localhost", 1.14) + with pytest.raises(TypeError): + MongoClient("localhost", []) + + with pytest.raises(ConfigurationError): + MongoClient([]) + + def test_max_pool_size_zero(self, simple_client): + simple_client(maxPoolSize=0) def test_uri_detection(self): - self.assertRaises(ConfigurationError, MongoClient, "/foo") - self.assertRaises(ConfigurationError, MongoClient, "://") - self.assertRaises(ConfigurationError, MongoClient, "foo/") - - def test_get_db(self): + with pytest.raises(ConfigurationError): + MongoClient("/foo") + with pytest.raises(ConfigurationError): + MongoClient("://") + with pytest.raises(ConfigurationError): + MongoClient("foo/") + + def test_get_db(self, client): def make_db(base, name): return base[name] - self.assertRaises(InvalidName, make_db, self.client, "") - self.assertRaises(InvalidName, make_db, self.client, "te$t") - self.assertRaises(InvalidName, make_db, self.client, "te.t") - self.assertRaises(InvalidName, make_db, self.client, "te\\t") - self.assertRaises(InvalidName, make_db, self.client, "te/t") - self.assertRaises(InvalidName, make_db, self.client, "te st") - - self.assertTrue(isinstance(self.client.test, Database)) - self.assertEqual(self.client.test, self.client["test"]) - self.assertEqual(self.client.test, Database(self.client, "test")) - - def test_get_database(self): + with pytest.raises(InvalidName): + make_db(client, "") + with pytest.raises(InvalidName): + make_db(client, "te$t") + with pytest.raises(InvalidName): + make_db(client, "te.t") + with pytest.raises(InvalidName): + make_db(client, "te\\t") + with pytest.raises(InvalidName): + make_db(client, "te/t") + with pytest.raises(InvalidName): + make_db(client, "te st") + # Type and equality assertions + assert isinstance(client.test, Database) + assert client.test == client["test"] + assert client.test == Database(client, "test") + + def test_get_database(self, client): codec_options = CodecOptions(tz_aware=True) write_concern = WriteConcern(w=2, j=True) - db = self.client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) - self.assertEqual("foo", db.name) - self.assertEqual(codec_options, db.codec_options) - self.assertEqual(ReadPreference.SECONDARY, db.read_preference) - self.assertEqual(write_concern, db.write_concern) + db = client.get_database("foo", codec_options, ReadPreference.SECONDARY, write_concern) + assert db.name == "foo" + assert db.codec_options == codec_options + assert db.read_preference == ReadPreference.SECONDARY + assert db.write_concern == write_concern - def test_getattr(self): - self.assertTrue(isinstance(self.client["_does_not_exist"], Database)) + def test_getattr(self, client): + assert isinstance(client["_does_not_exist"], Database) - with self.assertRaises(AttributeError) as context: - self.client._does_not_exist + with pytest.raises(AttributeError) as context: + client.client._does_not_exist # Message should be: # "AttributeError: MongoClient has no attribute '_does_not_exist'. To # access the _does_not_exist database, use client['_does_not_exist']". - self.assertIn("has no attribute '_does_not_exist'", str(context.exception)) - - def test_iteration(self): - client = self.client - msg = "'MongoClient' object is not iterable" - # Iteration fails - with self.assertRaisesRegex(TypeError, msg): - for _ in client: # type: ignore[misc] # error: "None" not callable [misc] + assert "has no attribute '_does_not_exist'" in str(context.value) + + def test_iteration(self, client): + if _IS_SYNC: + msg = "'MongoClient' object is not iterable" + else: + msg = "'MongoClient' object is not an async iterator" + + with pytest.raises(TypeError, match="'MongoClient' object is not iterable"): + for _ in client: break + # Index fails - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = client[0] - # next fails - with self.assertRaisesRegex(TypeError, "'MongoClient' object is not iterable"): + + # 'next' function fails + with pytest.raises(TypeError, match=msg): _ = next(client) - # .next() fails - with self.assertRaisesRegex(TypeError, "'MongoClient' object is not iterable"): + + # 'next()' method fails + with pytest.raises(TypeError, match="'MongoClient' object is not iterable"): _ = client.next() - # Do not implement typing.Iterable. - self.assertNotIsInstance(client, Iterable) - def test_get_default_database(self): - c = self.rs_or_single_client( - "mongodb://%s:%d/foo" % (client_context.host, client_context.port), + # Do not implement typing.Iterable + assert not isinstance(client, Iterable) + + def test_get_default_database(self, rs_or_single_client, client_context_fixture): + c = rs_or_single_client( + "mongodb://%s:%d/foo" % (client_context_fixture.host, client_context_fixture.port), connect=False, ) - self.assertEqual(Database(c, "foo"), c.get_default_database()) + assert Database(c, "foo") == c.get_default_database() # Test that default doesn't override the URI value. - self.assertEqual(Database(c, "foo"), c.get_default_database("bar")) - + assert Database(c, "foo") == c.get_default_database("bar") codec_options = CodecOptions(tz_aware=True) write_concern = WriteConcern(w=2, j=True) db = c.get_default_database(None, codec_options, ReadPreference.SECONDARY, write_concern) - self.assertEqual("foo", db.name) - self.assertEqual(codec_options, db.codec_options) - self.assertEqual(ReadPreference.SECONDARY, db.read_preference) - self.assertEqual(write_concern, db.write_concern) + assert "foo" == db.name + assert codec_options == db.codec_options + assert ReadPreference.SECONDARY == db.read_preference + assert write_concern == db.write_concern - c = self.rs_or_single_client( - "mongodb://%s:%d/" % (client_context.host, client_context.port), + c = rs_or_single_client( + "mongodb://%s:%d/" % (client_context_fixture.host, client_context_fixture.port), connect=False, ) - self.assertEqual(Database(c, "foo"), c.get_default_database("foo")) + assert Database(c, "foo") == c.get_default_database("foo") - def test_get_default_database_error(self): + def test_get_default_database_error(self, rs_or_single_client, client_context_fixture): # URI with no database. - c = self.rs_or_single_client( - "mongodb://%s:%d/" % (client_context.host, client_context.port), + c = rs_or_single_client( + "mongodb://%s:%d/" % (client_context_fixture.host, client_context_fixture.port), connect=False, ) - self.assertRaises(ConfigurationError, c.get_default_database) + with pytest.raises(ConfigurationError): + c.get_default_database() - def test_get_default_database_with_authsource(self): + def test_get_default_database_with_authsource( + self, client_context_fixture, rs_or_single_client + ): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( - client_context.host, - client_context.port, + client_context_fixture.host, + client_context_fixture.port, ) - c = self.rs_or_single_client(uri, connect=False) - self.assertEqual(Database(c, "foo"), c.get_default_database()) + c = rs_or_single_client(uri, connect=False) + assert Database(c, "foo") == c.get_default_database() - def test_get_database_default(self): - c = self.rs_or_single_client( - "mongodb://%s:%d/foo" % (client_context.host, client_context.port), + def test_get_database_default(self, client_context_fixture, rs_or_single_client): + c = rs_or_single_client( + "mongodb://%s:%d/foo" % (client_context_fixture.host, client_context_fixture.port), connect=False, ) - self.assertEqual(Database(c, "foo"), c.get_database()) + assert Database(c, "foo") == c.get_database() - def test_get_database_default_error(self): + def test_get_database_default_error(self, client_context_fixture, rs_or_single_client): # URI with no database. - c = self.rs_or_single_client( - "mongodb://%s:%d/" % (client_context.host, client_context.port), + c = rs_or_single_client( + "mongodb://%s:%d/" % (client_context_fixture.host, client_context_fixture.port), connect=False, ) - self.assertRaises(ConfigurationError, c.get_database) + with pytest.raises(ConfigurationError): + c.get_database() - def test_get_database_default_with_authsource(self): + def test_get_database_default_with_authsource( + self, client_context_fixture, rs_or_single_client + ): # Ensure we distinguish database name from authSource. uri = "mongodb://%s:%d/foo?authSource=src" % ( - client_context.host, - client_context.port, + client_context_fixture.host, + client_context_fixture.port, ) - c = self.rs_or_single_client(uri, connect=False) - self.assertEqual(Database(c, "foo"), c.get_database()) + c = rs_or_single_client(uri, connect=False) + assert Database(c, "foo") == c.get_database() - def test_primary_read_pref_with_tags(self): + def test_primary_read_pref_with_tags(self, single_client): # No tags allowed with "primary". - with self.assertRaises(ConfigurationError): - self.single_client("mongodb://host/?readpreferencetags=dc:east") - - with self.assertRaises(ConfigurationError): - self.single_client("mongodb://host/?readpreference=primary&readpreferencetags=dc:east") + with pytest.raises(ConfigurationError): + with single_client("mongodb://host/?readpreferencetags=dc:east"): + pass + with pytest.raises(ConfigurationError): + with single_client("mongodb://host/?readpreference=primary&readpreferencetags=dc:east"): + pass - def test_read_preference(self): - c = self.rs_or_single_client( + def test_read_preference(self, client_context_fixture, rs_or_single_client): + c = rs_or_single_client( "mongodb://host", connect=False, readpreference=ReadPreference.NEAREST.mongos_mode ) - self.assertEqual(c.read_preference, ReadPreference.NEAREST) + assert c.read_preference == ReadPreference.NEAREST - def test_metadata(self): + def test_metadata(self, simple_client): metadata = copy.deepcopy(_METADATA) if has_c(): metadata["driver"]["name"] = "PyMongo|c" else: metadata["driver"]["name"] = "PyMongo" metadata["application"] = {"name": "foobar"} - client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + + client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") options = client.options - self.assertEqual(options.pool_options.metadata, metadata) - client = self.simple_client("foo", 27017, appname="foobar", connect=False) + assert options.pool_options.metadata == metadata + + client = simple_client("foo", 27017, appname="foobar", connect=False) options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata + # No error - self.simple_client(appname="x" * 128) - with self.assertRaises(ValueError): - self.simple_client(appname="x" * 129) - # Bad "driver" options. - self.assertRaises(TypeError, DriverInfo, "Foo", 1, "a") - self.assertRaises(TypeError, DriverInfo, version="1", platform="a") - self.assertRaises(TypeError, DriverInfo) - with self.assertRaises(TypeError): - self.simple_client(driver=1) - with self.assertRaises(TypeError): - self.simple_client(driver="abc") - with self.assertRaises(TypeError): - self.simple_client(driver=("Foo", "1", "a")) - # Test appending to driver info. + simple_client(appname="x" * 128) + with pytest.raises(ValueError): + simple_client(appname="x" * 129) + + # Bad "driver" options. + with pytest.raises(TypeError): + DriverInfo("Foo", 1, "a") + with pytest.raises(TypeError): + DriverInfo(version="1", platform="a") + with pytest.raises(TypeError): + DriverInfo() + with pytest.raises(TypeError): + simple_client(driver=1) + with pytest.raises(TypeError): + simple_client(driver="abc") + with pytest.raises(TypeError): + simple_client(driver=("Foo", "1", "a")) + + # Test appending to driver info. if has_c(): metadata["driver"]["name"] = "PyMongo|c|FooDriver" else: metadata["driver"]["name"] = "PyMongo|FooDriver" metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"]) - client = self.simple_client( + + client = simple_client( "foo", 27017, appname="foobar", @@ -370,9 +398,10 @@ def test_metadata(self): connect=False, ) options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata + metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"]) - client = self.simple_client( + client = simple_client( "foo", 27017, appname="foobar", @@ -380,38 +409,35 @@ def test_metadata(self): connect=False, ) options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata + # Test truncating driver info metadata. - client = self.simple_client( + client = simple_client( driver=DriverInfo(name="s" * _MAX_METADATA_SIZE), connect=False, ) options = client.options - self.assertLessEqual( - len(bson.encode(options.pool_options.metadata)), - _MAX_METADATA_SIZE, - ) - client = self.simple_client( + assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE + + client = simple_client( driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE), connect=False, ) options = client.options - self.assertLessEqual( - len(bson.encode(options.pool_options.metadata)), - _MAX_METADATA_SIZE, - ) + assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) - def test_container_metadata(self): + def test_container_metadata(self, simple_client): metadata = copy.deepcopy(_METADATA) metadata["driver"]["name"] = "PyMongo" metadata["env"] = {} metadata["env"]["container"] = {"orchestrator": "kubernetes"} - client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + + client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") options = client.options - self.assertEqual(options.pool_options.metadata["env"], metadata["env"]) + assert options.pool_options.metadata["env"] == metadata["env"] - def test_kwargs_codec_options(self): + def test_kwargs_codec_options(self, simple_client): class MyFloatType: def __init__(self, x): self.__x = x @@ -433,7 +459,7 @@ def transform_python(self, value): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" tzinfo = utc - c = self.simple_client( + c = simple_client( document_class=document_class, type_registry=type_registry, tz_aware=tz_aware, @@ -442,18 +468,16 @@ def transform_python(self, value): tzinfo=tzinfo, connect=False, ) - self.assertEqual(c.codec_options.document_class, document_class) - self.assertEqual(c.codec_options.type_registry, type_registry) - self.assertEqual(c.codec_options.tz_aware, tz_aware) - self.assertEqual( - c.codec_options.uuid_representation, - _UUID_REPRESENTATIONS[uuid_representation_label], + assert c.codec_options.document_class == document_class + assert c.codec_options.type_registry == type_registry + assert c.codec_options.tz_aware == tz_aware + assert ( + c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] ) - self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) - self.assertEqual(c.codec_options.tzinfo, tzinfo) + assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler + assert c.codec_options.tzinfo == tzinfo - def test_uri_codec_options(self): - # Ensure codec options are passed in correctly + def test_uri_codec_options(self, client_context_fixture, simple_client): uuid_representation_label = "javaLegacy" unicode_decode_error_handler = "ignore" datetime_conversion = "DATETIME_CLAMP" @@ -462,57 +486,36 @@ def test_uri_codec_options(self): "%s&unicode_decode_error_handler=%s" "&datetime_conversion=%s" % ( - client_context.host, - client_context.port, + client_context_fixture.host, + client_context_fixture.port, uuid_representation_label, unicode_decode_error_handler, datetime_conversion, ) ) - c = self.simple_client(uri, connect=False) - self.assertEqual(c.codec_options.tz_aware, True) - self.assertEqual( - c.codec_options.uuid_representation, - _UUID_REPRESENTATIONS[uuid_representation_label], - ) - self.assertEqual(c.codec_options.unicode_decode_error_handler, unicode_decode_error_handler) - self.assertEqual( - c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] + c = simple_client(uri, connect=False) + assert c.codec_options.tz_aware is True + assert ( + c.codec_options.uuid_representation == _UUID_REPRESENTATIONS[uuid_representation_label] ) - + assert c.codec_options.unicode_decode_error_handler == unicode_decode_error_handler + assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] # Change the passed datetime_conversion to a number and re-assert. uri = uri.replace(datetime_conversion, f"{int(DatetimeConversion[datetime_conversion])}") - c = self.simple_client(uri, connect=False) - self.assertEqual( - c.codec_options.datetime_conversion, DatetimeConversion[datetime_conversion] - ) + c = simple_client(uri, connect=False) + assert c.codec_options.datetime_conversion == DatetimeConversion[datetime_conversion] - def test_uri_option_precedence(self): + def test_uri_option_precedence(self, simple_client): # Ensure kwarg options override connection string options. uri = "mongodb://localhost/?ssl=true&replicaSet=name&readPreference=primary" - c = self.simple_client( - uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred" - ) + c = simple_client(uri, ssl=False, replicaSet="newname", readPreference="secondaryPreferred") clopts = c.options opts = clopts._options + assert opts["tls"] is False + assert clopts.replica_set_name == "newname" + assert clopts.read_preference == ReadPreference.SECONDARY_PREFERRED - self.assertEqual(opts["tls"], False) - self.assertEqual(clopts.replica_set_name, "newname") - self.assertEqual(clopts.read_preference, ReadPreference.SECONDARY_PREFERRED) - - def test_connection_timeout_ms_propagates_to_DNS_resolver(self): - # Patch the resolver. - from pymongo.srv_resolver import _resolve - - patched_resolver = FunctionCallRecorder(_resolve) - pymongo.srv_resolver._resolve = patched_resolver - - def reset_resolver(): - pymongo.srv_resolver._resolve = _resolve - - self.addCleanup(reset_resolver) - - # Setup. + def test_connection_timeout_ms_propagates_to_DNS_resolver(self, patch_resolver, simple_client): base_uri = "mongodb+srv://test5.test.build.10gen.cc" connectTimeoutMS = 5000 expected_kw_value = 5.0 @@ -520,10 +523,10 @@ def reset_resolver(): expected_uri_value = 6.0 def test_scenario(args, kwargs, expected_value): - patched_resolver.reset() - self.simple_client(*args, **kwargs) - for _, kw in patched_resolver.call_list(): - self.assertAlmostEqual(kw["lifetime"], expected_value) + patch_resolver.reset() + simple_client(*args, **kwargs) + for _, kw in patch_resolver.call_list(): + assert pytest.approx(kw["lifetime"], rel=1e-6) == expected_value # No timeout specified. test_scenario((base_uri,), {}, CONNECT_TIMEOUT) @@ -538,38 +541,38 @@ def test_scenario(args, kwargs, expected_value): # Timeout specified in both kwargs and connection string. test_scenario((uri_with_timeout,), kwarg, expected_kw_value) - def test_uri_security_options(self): + def test_uri_security_options(self, simple_client): # Ensure that we don't silently override security-related options. - with self.assertRaises(InvalidURI): - self.simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) + with pytest.raises(InvalidURI): + simple_client("mongodb://localhost/?ssl=true", tls=False, connect=False) # Matching SSL and TLS options should not cause errors. - c = self.simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) - self.assertEqual(c.options._options["tls"], False) + c = simple_client("mongodb://localhost/?ssl=false", tls=False, connect=False) + assert c.options._options["tls"] is False # Conflicting tlsInsecure options should raise an error. - with self.assertRaises(InvalidURI): - self.simple_client( + with pytest.raises(InvalidURI): + simple_client( "mongodb://localhost/?tlsInsecure=true", connect=False, tlsAllowInvalidHostnames=True, ) # Conflicting legacy tlsInsecure options should also raise an error. - with self.assertRaises(InvalidURI): - self.simple_client( + with pytest.raises(InvalidURI): + simple_client( "mongodb://localhost/?tlsInsecure=true", connect=False, tlsAllowInvalidCertificates=False, ) # Conflicting kwargs should raise InvalidURI - with self.assertRaises(InvalidURI): - self.simple_client(ssl=True, tls=False) + with pytest.raises(InvalidURI): + simple_client(ssl=True, tls=False) - def test_event_listeners(self): - c = self.simple_client(event_listeners=[], connect=False) - self.assertEqual(c.options.event_listeners, []) + def test_event_listeners(self, simple_client): + c = simple_client(event_listeners=[], connect=False) + assert c.options.event_listeners == [] listeners = [ event_loggers.CommandLogger(), event_loggers.HeartbeatLogger(), @@ -577,28 +580,30 @@ def test_event_listeners(self): event_loggers.TopologyLogger(), event_loggers.ConnectionPoolLogger(), ] - c = self.simple_client(event_listeners=listeners, connect=False) - self.assertEqual(c.options.event_listeners, listeners) - - def test_client_options(self): - c = self.simple_client(connect=False) - self.assertIsInstance(c.options, ClientOptions) - self.assertIsInstance(c.options.pool_options, PoolOptions) - self.assertEqual(c.options.server_selection_timeout, 30) - self.assertEqual(c.options.pool_options.max_idle_time_seconds, None) - self.assertIsInstance(c.options.retry_writes, bool) - self.assertIsInstance(c.options.retry_reads, bool) + c = simple_client(event_listeners=listeners, connect=False) + assert c.options.event_listeners == listeners + + def test_client_options(self, simple_client): + c = simple_client(connect=False) + assert isinstance(c.options, ClientOptions) + assert isinstance(c.options.pool_options, PoolOptions) + assert c.options.server_selection_timeout == 30 + assert c.options.pool_options.max_idle_time_seconds is None + assert isinstance(c.options.retry_writes, bool) + assert isinstance(c.options.retry_reads, bool) def test_validate_suggestion(self): """Validate kwargs in constructor.""" for typo in ["auth", "Auth", "AUTH"]: - expected = f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." + expected = ( + f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, " + f"authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring." + ) expected = re.escape(expected) - with self.assertRaisesRegex(ConfigurationError, expected): + with pytest.raises(ConfigurationError, match=expected): MongoClient(**{typo: "standard"}) # type: ignore[arg-type] - @patch("pymongo.srv_resolver._SrvResolver.get_hosts") - def test_detected_environment_logging(self, mock_get_hosts): + def test_detected_environment_logging(self, caplog): normal_hosts = [ "normal.host.com", "host.cosmos.azure.com", @@ -609,42 +614,47 @@ def test_detected_environment_logging(self, mock_get_hosts): multi_host = ( "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" ) - with self.assertLogs("pymongo", level="INFO") as cm: - for host in normal_hosts: - MongoClient(host, connect=False) - for host in srv_hosts: - mock_get_hosts.return_value = [(host, 1)] - MongoClient(host, connect=False) - MongoClient(multi_host, connect=False) - logs = [record.getMessage() for record in cm.records if record.name == "pymongo.client"] - self.assertEqual(len(logs), 7) - - @patch("pymongo.srv_resolver._SrvResolver.get_hosts") - def test_detected_environment_warning(self, mock_get_hosts): - with self._caplog.at_level(logging.WARN): - normal_hosts = [ - "host.cosmos.azure.com", - "host.docdb.amazonaws.com", - "host.docdb-elastic.amazonaws.com", - ] - srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] - multi_host = ( - "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" - ) - for host in normal_hosts: - with self.assertWarns(UserWarning): - self.simple_client(host) - for host in srv_hosts: - mock_get_hosts.return_value = [(host, 1)] - with self.assertWarns(UserWarning): - self.simple_client(host) - with self.assertWarns(UserWarning): - self.simple_client(multi_host) - - -class TestClient(IntegrationTest): + with caplog.at_level(logging.INFO, logger="pymongo"): + with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: + for host in normal_hosts: + MongoClient(host, connect=False) + for host in srv_hosts: + mock_get_hosts.return_value = [(host, 1)] + MongoClient(host, connect=False) + MongoClient(multi_host, connect=False) + logs = [ + record.getMessage() + for record in caplog.records + if record.name == "pymongo.client" + ] + assert len(logs) == 7 + + def test_detected_environment_warning(self, caplog, simple_client): + normal_hosts = [ + "host.cosmos.azure.com", + "host.docdb.amazonaws.com", + "host.docdb-elastic.amazonaws.com", + ] + srv_hosts = ["mongodb+srv://:@" + s for s in normal_hosts] + multi_host = ( + "host.cosmos.azure.com,host.docdb.amazonaws.com,host.docdb-elastic.amazonaws.com" + ) + with caplog.at_level(logging.WARN, logger="pymongo"): + with mock.patch("pymongo.srv_resolver._SrvResolver.get_hosts") as mock_get_hosts: + with pytest.warns(UserWarning): + for host in normal_hosts: + simple_client(host) + for host in srv_hosts: + mock_get_hosts.return_value = [(host, 1)] + simple_client(host) + simple_client(multi_host) + + +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestClientIntegrationTest(PyMongoTestCasePyTest): def test_multiple_uris(self): - with self.assertRaises(ConfigurationError): + with pytest.raises(ConfigurationError): MongoClient( host=[ "mongodb+srv://cluster-a.abc12.mongodb.net", @@ -653,207 +663,208 @@ def test_multiple_uris(self): ] ) - def test_max_idle_time_reaper_default(self): + def test_max_idle_time_reaper_default(self, rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): # Assert reaper doesn't remove connections when maxIdleTimeMS not set - client = self.rs_or_single_client() + client = rs_or_single_client() server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) with server._pool.checkout() as conn: pass - self.assertEqual(1, len(server._pool.conns)) - self.assertTrue(conn in server._pool.conns) + assert 1 == len(server._pool.conns) + assert conn in server._pool.conns - def test_max_idle_time_reaper_removes_stale_minPoolSize(self): + def test_max_idle_time_reaper_removes_stale_minPoolSize(self, rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): # Assert reaper removes idle socket and replaces it with a new one - client = self.rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1) + client = rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1) server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) with server._pool.checkout() as conn: pass # When the reaper runs at the same time as the get_socket, two # connections could be created and checked into the pool. - self.assertGreaterEqual(len(server._pool.conns), 1) + assert len(server._pool.conns) >= 1 wait_until(lambda: conn not in server._pool.conns, "remove stale socket") wait_until(lambda: len(server._pool.conns) >= 1, "replace stale socket") - def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self): + def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self, rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): # Assert reaper respects maxPoolSize when adding new connections. - client = self.rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1, maxPoolSize=1) + client = rs_or_single_client(maxIdleTimeMS=500, minPoolSize=1, maxPoolSize=1) server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) with server._pool.checkout() as conn: pass # When the reaper runs at the same time as the get_socket, # maxPoolSize=1 should prevent two connections from being created. - self.assertEqual(1, len(server._pool.conns)) + assert 1 == len(server._pool.conns) wait_until(lambda: conn not in server._pool.conns, "remove stale socket") wait_until(lambda: len(server._pool.conns) == 1, "replace stale socket") - def test_max_idle_time_reaper_removes_stale(self): + def test_max_idle_time_reaper_removes_stale(self, rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): - # Assert reaper has removed idle socket and NOT replaced it - client = self.rs_or_single_client(maxIdleTimeMS=500) + # Assert that the reaper has removed the idle socket and NOT replaced it. + client = rs_or_single_client(maxIdleTimeMS=500) server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) with server._pool.checkout() as conn_one: pass - # Assert that the pool does not close connections prematurely. + # Assert that the pool does not close connections prematurely time.sleep(0.300) with server._pool.checkout() as conn_two: pass - self.assertIs(conn_one, conn_two) + assert conn_one is conn_two wait_until( lambda: len(server._pool.conns) == 0, "stale socket reaped and new one NOT added to the pool", ) - def test_min_pool_size(self): + def test_min_pool_size(self, rs_or_single_client): with client_knobs(kill_cursor_frequency=0.1): - client = self.rs_or_single_client() + client = rs_or_single_client() server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) - self.assertEqual(0, len(server._pool.conns)) + assert len(server._pool.conns) == 0 # Assert that pool started up at minPoolSize - client = self.rs_or_single_client(minPoolSize=10) + client = rs_or_single_client(minPoolSize=10) server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) wait_until( lambda: len(server._pool.conns) == 10, "pool initialized with 10 connections", ) - - # Assert that if a socket is closed, a new one takes its place + # Assert that if a socket is closed, a new one takes its place. with server._pool.checkout() as conn: conn.close_conn(None) wait_until( lambda: len(server._pool.conns) == 10, "a closed socket gets replaced from the pool", ) - self.assertFalse(conn in server._pool.conns) + assert conn not in server._pool.conns - def test_max_idle_time_checkout(self): + def test_max_idle_time_checkout(self, rs_or_single_client): # Use high frequency to test _get_socket_no_auth. with client_knobs(kill_cursor_frequency=99999999): - client = self.rs_or_single_client(maxIdleTimeMS=500) + client = rs_or_single_client(maxIdleTimeMS=500) server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) with server._pool.checkout() as conn: pass - self.assertEqual(1, len(server._pool.conns)) + assert len(server._pool.conns) == 1 time.sleep(1) # Sleep so that the socket becomes stale. - with server._pool.checkout() as new_con: - self.assertNotEqual(conn, new_con) - self.assertEqual(1, len(server._pool.conns)) - self.assertFalse(conn in server._pool.conns) - self.assertTrue(new_con in server._pool.conns) + with server._pool.checkout() as new_conn: + assert conn != new_conn + assert len(server._pool.conns) == 1 + assert conn not in server._pool.conns + assert new_conn in server._pool.conns # Test that connections are reused if maxIdleTimeMS is not set. - client = self.rs_or_single_client() + client = rs_or_single_client() server = (client._get_topology()).select_server(readable_server_selector, _Op.TEST) with server._pool.checkout() as conn: pass - self.assertEqual(1, len(server._pool.conns)) + assert len(server._pool.conns) == 1 time.sleep(1) - with server._pool.checkout() as new_con: - self.assertEqual(conn, new_con) - self.assertEqual(1, len(server._pool.conns)) + with server._pool.checkout() as new_conn: + assert conn == new_conn + assert len(server._pool.conns) == 1 - def test_constants(self): + def test_constants(self, client_context_fixture, simple_client): """This test uses MongoClient explicitly to make sure that host and port are not overloaded. """ - host, port = client_context.host, client_context.port - kwargs: dict = client_context.default_client_options.copy() - if client_context.auth_enabled: + host, port = ( + client_context_fixture.host, + client_context_fixture.port, + ) + kwargs: dict = client_context_fixture.default_client_options.copy() + if client_context_fixture.auth_enabled: kwargs["username"] = db_user kwargs["password"] = db_pwd # Set bad defaults. MongoClient.HOST = "somedomainthatdoesntexist.org" MongoClient.PORT = 123456789 - with self.assertRaises(AutoReconnect): - c = self.simple_client(serverSelectionTimeoutMS=10, **kwargs) + with pytest.raises(AutoReconnect): + c = simple_client(serverSelectionTimeoutMS=10, **kwargs) connected(c) - - c = self.simple_client(host, port, **kwargs) + c = simple_client(host, port, **kwargs) # Override the defaults. No error. connected(c) - # Set good defaults. MongoClient.HOST = host MongoClient.PORT = port - # No error. - c = self.simple_client(**kwargs) + c = simple_client(**kwargs) connected(c) - def test_init_disconnected(self): - host, port = client_context.host, client_context.port - c = self.rs_or_single_client(connect=False) + def test_init_disconnected(self, client_context_fixture, rs_or_single_client, simple_client): + host, port = ( + client_context_fixture.host, + client_context_fixture.port, + ) + c = rs_or_single_client(connect=False) # is_primary causes client to block until connected - self.assertIsInstance(c.is_primary, bool) - c = self.rs_or_single_client(connect=False) - self.assertIsInstance(c.is_mongos, bool) - c = self.rs_or_single_client(connect=False) - self.assertIsInstance(c.options.pool_options.max_pool_size, int) - self.assertIsInstance(c.nodes, frozenset) - - c = self.rs_or_single_client(connect=False) - self.assertEqual(c.codec_options, CodecOptions()) - c = self.rs_or_single_client(connect=False) - self.assertFalse(c.primary) - self.assertFalse(c.secondaries) - c = self.rs_or_single_client(connect=False) - self.assertIsInstance(c.topology_description, TopologyDescription) - self.assertEqual(c.topology_description, c._topology._description) - if client_context.is_rs: + assert isinstance(c.is_primary, bool) + c = rs_or_single_client(connect=False) + assert isinstance(c.is_mongos, bool) + c = rs_or_single_client(connect=False) + assert isinstance(c.options.pool_options.max_pool_size, int) + assert isinstance(c.nodes, frozenset) + + c = rs_or_single_client(connect=False) + assert c.codec_options == CodecOptions() + c = rs_or_single_client(connect=False) + assert not c.primary + assert not c.secondaries + c = rs_or_single_client(connect=False) + assert isinstance(c.topology_description, TopologyDescription) + assert c.topology_description == c._topology._description + if client_context_fixture.is_rs: # The primary's host and port are from the replica set config. - self.assertIsNotNone(c.address) + assert c.address is not None else: - self.assertEqual(c.address, (host, port)) - + assert c.address == (host, port) bad_host = "somedomainthatdoesntexist.org" - c = self.simple_client(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10) - with self.assertRaises(ConnectionFailure): + c = simple_client(bad_host, port, connectTimeoutMS=1, serverSelectionTimeoutMS=10) + with pytest.raises(ConnectionFailure): c.pymongo_test.test.find_one() - def test_init_disconnected_with_auth(self): + def test_init_disconnected_with_auth(self, simple_client): uri = "mongodb://user:pass@somedomainthatdoesntexist" - c = self.simple_client(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10) - with self.assertRaises(ConnectionFailure): + c = simple_client(uri, connectTimeoutMS=1, serverSelectionTimeoutMS=10) + with pytest.raises(ConnectionFailure): c.pymongo_test.test.find_one() - def test_equality(self): - seed = "{}:{}".format(*list(self.client._topology_settings.seeds)[0]) - c = self.rs_or_single_client(seed, connect=False) - self.assertEqual(client_context.client, c) + def test_equality(self, client_context_fixture, rs_or_single_client, simple_client): + seed = "{}:{}".format(*list(client_context_fixture.client._topology_settings.seeds)[0]) + c = rs_or_single_client(seed, connect=False) + assert client_context_fixture.client == c # Explicitly test inequality - self.assertFalse(client_context.client != c) + assert not client_context_fixture.client != c - c = self.rs_or_single_client("invalid.com", connect=False) - self.assertNotEqual(client_context.client, c) - self.assertTrue(client_context.client != c) + c = rs_or_single_client("invalid.com", connect=False) + assert client_context_fixture.client != c + assert client_context_fixture.client != c - c1 = self.simple_client("a", connect=False) - c2 = self.simple_client("b", connect=False) + c1 = simple_client("a", connect=False) + c2 = simple_client("b", connect=False) # Seeds differ: - self.assertNotEqual(c1, c2) + assert c1 != c2 - c1 = self.simple_client(["a", "b", "c"], connect=False) - c2 = self.simple_client(["c", "a", "b"], connect=False) + c1 = simple_client(["a", "b", "c"], connect=False) + c2 = simple_client(["c", "a", "b"], connect=False) # Same seeds but out of order still compares equal: - self.assertEqual(c1, c2) - - def test_hashable(self): - seed = "{}:{}".format(*list(self.client._topology_settings.seeds)[0]) - c = self.rs_or_single_client(seed, connect=False) - self.assertIn(c, {client_context.client}) - c = self.rs_or_single_client("invalid.com", connect=False) - self.assertNotIn(c, {client_context.client}) - - def test_host_w_port(self): - with self.assertRaises(ValueError): - host = client_context.host + assert c1 == c2 + + def test_hashable(self, client_context_fixture, rs_or_single_client): + seed = "{}:{}".format(*list(client_context_fixture.client._topology_settings.seeds)[0]) + c = rs_or_single_client(seed, connect=False) + assert c in {client_context_fixture.client} + c = rs_or_single_client("invalid.com", connect=False) + assert c not in {client_context_fixture.client} + + def test_host_w_port(self, client_context_fixture): + with pytest.raises(ValueError): + host = client_context_fixture.host connected( MongoClient( f"{host}:1234567", @@ -862,7 +873,7 @@ def test_host_w_port(self): ) ) - def test_repr(self): + def test_repr(self, simple_client): # Used to test 'eval' below. import bson @@ -872,19 +883,16 @@ def test_repr(self): connect=False, document_class=SON, ) - the_repr = repr(client) - self.assertIn("MongoClient(host=", the_repr) - self.assertIn("document_class=bson.son.SON, tz_aware=False, connect=False, ", the_repr) - self.assertIn("connecttimeoutms=12345", the_repr) - self.assertIn("replicaset='replset'", the_repr) - self.assertIn("w=1", the_repr) - self.assertIn("wtimeoutms=100", the_repr) - + assert "MongoClient(host=" in the_repr + assert "document_class=bson.son.SON, tz_aware=False, connect=False, " in the_repr + assert "connecttimeoutms=12345" in the_repr + assert "replicaset='replset'" in the_repr + assert "w=1" in the_repr + assert "wtimeoutms=100" in the_repr with eval(the_repr) as client_two: - self.assertEqual(client_two, client) - - client = self.simple_client( + assert client_two == client + client = simple_client( "localhost:27017,localhost:27018", replicaSet="replset", connectTimeoutMS=12345, @@ -894,91 +902,94 @@ def test_repr(self): connect=False, ) the_repr = repr(client) - self.assertIn("MongoClient(host=", the_repr) - self.assertIn("document_class=dict, tz_aware=False, connect=False, ", the_repr) - self.assertIn("connecttimeoutms=12345", the_repr) - self.assertIn("replicaset='replset'", the_repr) - self.assertIn("sockettimeoutms=None", the_repr) - self.assertIn("w=1", the_repr) - self.assertIn("wtimeoutms=100", the_repr) - + assert "MongoClient(host=" in the_repr + assert "document_class=dict, tz_aware=False, connect=False, " in the_repr + assert "connecttimeoutms=12345" in the_repr + assert "replicaset='replset'" in the_repr + assert "sockettimeoutms=None" in the_repr + assert "w=1" in the_repr + assert "wtimeoutms=100" in the_repr with eval(the_repr) as client_two: - self.assertEqual(client_two, client) + assert client_two == client - def test_getters(self): - wait_until(lambda: client_context.nodes == self.client.nodes, "find all nodes") + def test_getters(self, client_context_fixture): + wait_until( + lambda: client_context_fixture.nodes == client_context_fixture.client.nodes, + "find all nodes", + ) - def test_list_databases(self): - cmd_docs = (self.client.admin.command("listDatabases"))["databases"] - cursor = self.client.list_databases() - self.assertIsInstance(cursor, CommandCursor) + def test_list_databases(self, client_context_fixture, rs_or_single_client): + cmd_docs = (client_context_fixture.client.admin.command("listDatabases"))["databases"] + cursor = client_context_fixture.client.list_databases() + assert isinstance(cursor, CommandCursor) helper_docs = cursor.to_list() - self.assertTrue(len(helper_docs) > 0) - self.assertEqual(len(helper_docs), len(cmd_docs)) + assert len(helper_docs) > 0 + assert len(helper_docs) == len(cmd_docs) # PYTHON-3529 Some fields may change between calls, just compare names. for helper_doc, cmd_doc in zip(helper_docs, cmd_docs): - self.assertIs(type(helper_doc), dict) - self.assertEqual(helper_doc.keys(), cmd_doc.keys()) - client = self.rs_or_single_client(document_class=SON) - for doc in client.list_databases(): - self.assertIs(type(doc), dict) - - self.client.pymongo_test.test.insert_one({}) - cursor = self.client.list_databases(filter={"name": "admin"}) + assert isinstance(helper_doc, dict) + assert helper_doc.keys() == cmd_doc.keys() + + client_doc = rs_or_single_client(document_class=SON) + for doc in client_doc.list_databases(): + assert isinstance(doc, dict) + + client_context_fixture.client.pymongo_test.test.insert_one({}) + cursor = client_context_fixture.client.list_databases(filter={"name": "admin"}) docs = cursor.to_list() - self.assertEqual(1, len(docs)) - self.assertEqual(docs[0]["name"], "admin") + assert len(docs) == 1 + assert docs[0]["name"] == "admin" - cursor = self.client.list_databases(nameOnly=True) + cursor = client_context_fixture.client.list_databases(nameOnly=True) for doc in cursor: - self.assertEqual(["name"], list(doc)) + assert list(doc) == ["name"] - def test_list_database_names(self): - self.client.pymongo_test.test.insert_one({"dummy": "object"}) - self.client.pymongo_test_mike.test.insert_one({"dummy": "object"}) - cmd_docs = (self.client.admin.command("listDatabases"))["databases"] + def test_list_database_names(self, client_context_fixture): + client_context_fixture.client.pymongo_test.test.insert_one({"dummy": "object"}) + client_context_fixture.client.pymongo_test_mike.test.insert_one({"dummy": "object"}) + cmd_docs = (client_context_fixture.client.admin.command("listDatabases"))["databases"] cmd_names = [doc["name"] for doc in cmd_docs] - db_names = self.client.list_database_names() - self.assertTrue("pymongo_test" in db_names) - self.assertTrue("pymongo_test_mike" in db_names) - self.assertEqual(db_names, cmd_names) - - def test_drop_database(self): - with self.assertRaises(TypeError): - self.client.drop_database(5) # type: ignore[arg-type] - with self.assertRaises(TypeError): - self.client.drop_database(None) # type: ignore[arg-type] - - self.client.pymongo_test.test.insert_one({"dummy": "object"}) - self.client.pymongo_test2.test.insert_one({"dummy": "object"}) - dbs = self.client.list_database_names() - self.assertIn("pymongo_test", dbs) - self.assertIn("pymongo_test2", dbs) - self.client.drop_database("pymongo_test") - - if client_context.is_rs: - wc_client = self.rs_or_single_client(w=len(client_context.nodes) + 1) - with self.assertRaises(WriteConcernError): + db_names = client_context_fixture.client.list_database_names() + assert "pymongo_test" in db_names + assert "pymongo_test_mike" in db_names + assert db_names == cmd_names + + def test_drop_database(self, client_context_fixture, rs_or_single_client): + with pytest.raises(TypeError): + client_context_fixture.client.drop_database(5) # type: ignore[arg-type] + with pytest.raises(TypeError): + client_context_fixture.client.drop_database(None) # type: ignore[arg-type] + + client_context_fixture.client.pymongo_test.test.insert_one({"dummy": "object"}) + client_context_fixture.client.pymongo_test2.test.insert_one({"dummy": "object"}) + dbs = client_context_fixture.client.list_database_names() + assert "pymongo_test" in dbs + assert "pymongo_test2" in dbs + client_context_fixture.client.drop_database("pymongo_test") + + if client_context_fixture.is_rs: + wc_client = rs_or_single_client(w=len(client_context_fixture.nodes) + 1) + with pytest.raises(WriteConcernError): wc_client.drop_database("pymongo_test2") - self.client.drop_database(self.client.pymongo_test2) - dbs = self.client.list_database_names() - self.assertNotIn("pymongo_test", dbs) - self.assertNotIn("pymongo_test2", dbs) + client_context_fixture.client.drop_database(client_context_fixture.client.pymongo_test2) + dbs = client_context_fixture.client.list_database_names() + assert "pymongo_test" not in dbs + assert "pymongo_test2" not in dbs - def test_close(self): - test_client = self.rs_or_single_client() + def test_close(self, rs_or_single_client): + test_client = rs_or_single_client() coll = test_client.pymongo_test.bar test_client.close() - with self.assertRaises(InvalidOperation): + with pytest.raises(InvalidOperation): coll.count_documents({}) - def test_close_kills_cursors(self): + def test_close_kills_cursors(self, rs_or_single_client): if sys.platform.startswith("java"): # We can't figure out how to make this test reliable with Jython. raise SkipTest("Can't test with Jython") - test_client = self.rs_or_single_client() + test_client = rs_or_single_client() # Kill any cursors possibly queued up by previous tests. gc.collect() test_client._process_periodic_tasks() @@ -990,238 +1001,250 @@ def test_close_kills_cursors(self): # Open a cursor and leave it open on the server. cursor = coll.find().batch_size(10) - self.assertTrue(bool(next(cursor))) - self.assertLess(cursor.retrieved, docs_inserted) + assert bool(next(cursor)) + assert cursor.retrieved < docs_inserted # Open a command cursor and leave it open on the server. cursor = coll.aggregate([], batchSize=10) - self.assertTrue(bool(next(cursor))) + assert bool(next(cursor)) del cursor # Required for PyPy, Jython and other Python implementations that # don't use reference counting garbage collection. gc.collect() # Close the client and ensure the topology is closed. - self.assertTrue(test_client._topology._opened) + assert test_client._topology._opened test_client.close() - self.assertFalse(test_client._topology._opened) - test_client = self.rs_or_single_client() + assert not test_client._topology._opened + test_client = rs_or_single_client() # The killCursors task should not need to re-open the topology. test_client._process_periodic_tasks() - self.assertTrue(test_client._topology._opened) + assert test_client._topology._opened - def test_close_stops_kill_cursors_thread(self): - client = self.rs_client() + def test_close_stops_kill_cursors_thread(self, rs_client): + client = rs_client() client.test.test.find_one() - self.assertFalse(client._kill_cursors_executor._stopped) + assert not client._kill_cursors_executor._stopped # Closing the client should stop the thread. client.close() - self.assertTrue(client._kill_cursors_executor._stopped) + assert client._kill_cursors_executor._stopped # Reusing the closed client should raise an InvalidOperation error. - with self.assertRaises(InvalidOperation): + with pytest.raises(InvalidOperation): client.admin.command("ping") # Thread is still stopped. - self.assertTrue(client._kill_cursors_executor._stopped) + assert client._kill_cursors_executor._stopped - def test_uri_connect_option(self): + def test_uri_connect_option(self, rs_client): # Ensure that topology is not opened if connect=False. - client = self.rs_client(connect=False) - self.assertFalse(client._topology._opened) + client = rs_client(connect=False) + assert not client._topology._opened # Ensure kill cursors thread has not been started. if _IS_SYNC: kc_thread = client._kill_cursors_executor._thread - self.assertFalse(kc_thread and kc_thread.is_alive()) + assert not (kc_thread and kc_thread.is_alive()) else: kc_task = client._kill_cursors_executor._task - self.assertFalse(kc_task and not kc_task.done()) + assert not (kc_task and not kc_task.done()) # Using the client should open topology and start the thread. client.admin.command("ping") - self.assertTrue(client._topology._opened) + assert client._topology._opened if _IS_SYNC: kc_thread = client._kill_cursors_executor._thread - self.assertTrue(kc_thread and kc_thread.is_alive()) + assert kc_thread and kc_thread.is_alive() else: kc_task = client._kill_cursors_executor._task - self.assertTrue(kc_task and not kc_task.done()) + assert kc_task and not kc_task.done() - def test_close_does_not_open_servers(self): - client = self.rs_client(connect=False) + def test_close_does_not_open_servers(self, rs_client): + client = rs_client(connect=False) topology = client._topology - self.assertEqual(topology._servers, {}) + assert topology._servers == {} client.close() - self.assertEqual(topology._servers, {}) + assert topology._servers == {} - def test_close_closes_sockets(self): - client = self.rs_client() + def test_close_closes_sockets(self, rs_client): + client = rs_client() client.test.test.find_one() topology = client._topology client.close() for server in topology._servers.values(): - self.assertFalse(server._pool.conns) - self.assertTrue(server._monitor._executor._stopped) - self.assertTrue(server._monitor._rtt_monitor._executor._stopped) - self.assertFalse(server._monitor._pool.conns) - self.assertFalse(server._monitor._rtt_monitor._pool.conns) + assert not server._pool.conns + assert server._monitor._executor._stopped + assert server._monitor._rtt_monitor._executor._stopped + assert not server._monitor._pool.conns + assert not server._monitor._rtt_monitor._pool.conns def test_bad_uri(self): - with self.assertRaises(InvalidURI): + with pytest.raises(InvalidURI): MongoClient("http://localhost") - @client_context.require_auth - @client_context.require_no_fips - def test_auth_from_uri(self): - host, port = client_context.host, client_context.port - client_context.create_user("admin", "admin", "pass") - self.addCleanup(client_context.drop_user, "admin", "admin") - self.addCleanup(remove_all_users, self.client.pymongo_test) + @pytest.mark.usefixtures("require_auth") + @pytest.mark.usefixtures("require_no_fips") + @pytest.mark.parametrize("remove_all_users_fixture", ["pymongo_test"], indirect=True) + @pytest.mark.parametrize("drop_user_fixture", [("admin", "admin")], indirect=True) + def test_auth_from_uri( + self, + client_context_fixture, + rs_or_single_client_noauth, + remove_all_users_fixture, + drop_user_fixture, + ): + host, port = ( + client_context_fixture.host, + client_context_fixture.port, + ) + client_context_fixture.create_user("admin", "admin", "pass") - client_context.create_user("pymongo_test", "user", "pass", roles=["userAdmin", "readWrite"]) + client_context_fixture.create_user( + "pymongo_test", "user", "pass", roles=["userAdmin", "readWrite"] + ) - with self.assertRaises(OperationFailure): - connected(self.rs_or_single_client_noauth("mongodb://a:b@%s:%d" % (host, port))) + with pytest.raises(OperationFailure): + connected(rs_or_single_client_noauth("mongodb://a:b@%s:%d" % (host, port))) # No error. - connected(self.rs_or_single_client_noauth("mongodb://admin:pass@%s:%d" % (host, port))) + connected(rs_or_single_client_noauth("mongodb://admin:pass@%s:%d" % (host, port))) # Wrong database. uri = "mongodb://admin:pass@%s:%d/pymongo_test" % (host, port) - with self.assertRaises(OperationFailure): - connected(self.rs_or_single_client_noauth(uri)) + with pytest.raises(OperationFailure): + connected(rs_or_single_client_noauth(uri)) # No error. connected( - self.rs_or_single_client_noauth("mongodb://user:pass@%s:%d/pymongo_test" % (host, port)) + rs_or_single_client_noauth("mongodb://user:pass@%s:%d/pymongo_test" % (host, port)) ) # Auth with lazy connection. ( - self.rs_or_single_client_noauth( + rs_or_single_client_noauth( "mongodb://user:pass@%s:%d/pymongo_test" % (host, port), connect=False ) ).pymongo_test.test.find_one() # Wrong password. - bad_client = self.rs_or_single_client_noauth( + bad_client = rs_or_single_client_noauth( "mongodb://user:wrong@%s:%d/pymongo_test" % (host, port), connect=False ) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): bad_client.pymongo_test.test.find_one() - @client_context.require_auth - def test_username_and_password(self): - client_context.create_user("admin", "ad min", "pa/ss") - self.addCleanup(client_context.drop_user, "admin", "ad min") + @pytest.mark.usefixtures("require_auth") + @pytest.mark.parametrize("drop_user_fixture", [("admin", "ad min")], indirect=True) + def test_username_and_password( + self, client_context_fixture, rs_or_single_client_noauth, drop_user_fixture + ): + client_context_fixture.create_user("admin", "ad min", "pa/ss") - c = self.rs_or_single_client_noauth(username="ad min", password="pa/ss") + c = rs_or_single_client_noauth(username="ad min", password="pa/ss") # Username and password aren't in strings that will likely be logged. - self.assertNotIn("ad min", repr(c)) - self.assertNotIn("ad min", str(c)) - self.assertNotIn("pa/ss", repr(c)) - self.assertNotIn("pa/ss", str(c)) + assert "ad min" not in repr(c) + assert "ad min" not in str(c) + assert "pa/ss" not in repr(c) + assert "pa/ss" not in str(c) # Auth succeeds. c.server_info() - with self.assertRaises(OperationFailure): - (self.rs_or_single_client_noauth(username="ad min", password="foo")).server_info() + with pytest.raises(OperationFailure): + (rs_or_single_client_noauth(username="ad min", password="foo")).server_info() - @client_context.require_auth - @client_context.require_no_fips - def test_lazy_auth_raises_operation_failure(self): - host = client_context.host - lazy_client = self.rs_or_single_client_noauth( + @pytest.mark.usefixtures("require_auth") + @pytest.mark.usefixtures("require_no_fips") + def test_lazy_auth_raises_operation_failure( + self, client_context_fixture, rs_or_single_client_noauth + ): + host = client_context_fixture.host + lazy_client = rs_or_single_client_noauth( f"mongodb://user:wrong@{host}/pymongo_test", connect=False ) assertRaisesExactly(OperationFailure, lazy_client.test.collection.find_one) - @client_context.require_no_tls - def test_unix_socket(self): + @pytest.mark.usefixtures("require_no_tls") + def test_unix_socket(self, client_context_fixture, rs_or_single_client, simple_client): if not hasattr(socket, "AF_UNIX"): - raise SkipTest("UNIX-sockets are not supported on this system") + pytest.skip("UNIX-sockets are not supported on this system") - mongodb_socket = "/tmp/mongodb-%d.sock" % (client_context.port,) - encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (client_context.port,) + mongodb_socket = "/tmp/mongodb-%d.sock" % (client_context_fixture.port,) + encoded_socket = "%2Ftmp%2F" + "mongodb-%d.sock" % (client_context_fixture.port,) if not os.access(mongodb_socket, os.R_OK): - raise SkipTest("Socket file is not accessible") + pytest.skip("Socket file is not accessible") uri = "mongodb://%s" % encoded_socket # Confirm we can do operations via the socket. - client = self.rs_or_single_client(uri) + client = rs_or_single_client(uri) client.pymongo_test.test.insert_one({"dummy": "object"}) dbs = client.list_database_names() - self.assertTrue("pymongo_test" in dbs) + assert "pymongo_test" in dbs - self.assertTrue(mongodb_socket in repr(client)) + assert mongodb_socket in repr(client) # Confirm it fails with a missing socket. - with self.assertRaises(ConnectionFailure): - c = self.simple_client( - "mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100 - ) + with pytest.raises(ConnectionFailure): + c = simple_client("mongodb://%2Ftmp%2Fnon-existent.sock", serverSelectionTimeoutMS=100) connected(c) - def test_document_class(self): - c = self.client + def test_document_class(self, client_context_fixture, rs_or_single_client): + c = client_context_fixture.client db = c.pymongo_test db.test.insert_one({"x": 1}) - self.assertEqual(dict, c.codec_options.document_class) - self.assertTrue(isinstance(db.test.find_one(), dict)) - self.assertFalse(isinstance(db.test.find_one(), SON)) + assert dict == c.codec_options.document_class + assert isinstance(db.test.find_one(), dict) + assert not isinstance(db.test.find_one(), SON) - c = self.rs_or_single_client(document_class=SON) + c = rs_or_single_client(document_class=SON) db = c.pymongo_test - self.assertEqual(SON, c.codec_options.document_class) - self.assertTrue(isinstance(db.test.find_one(), SON)) + assert SON == c.codec_options.document_class + assert isinstance(db.test.find_one(), SON) - def test_timeouts(self): - client = self.rs_or_single_client( + def test_timeouts(self, rs_or_single_client): + client = rs_or_single_client( connectTimeoutMS=10500, socketTimeoutMS=10500, maxIdleTimeMS=10500, serverSelectionTimeoutMS=10500, ) - self.assertEqual(10.5, (get_pool(client)).opts.connect_timeout) - self.assertEqual(10.5, (get_pool(client)).opts.socket_timeout) - self.assertEqual(10.5, (get_pool(client)).opts.max_idle_time_seconds) - self.assertEqual(10.5, client.options.pool_options.max_idle_time_seconds) - self.assertEqual(10.5, client.options.server_selection_timeout) + assert 10.5 == (get_pool(client)).opts.connect_timeout + assert 10.5 == (get_pool(client)).opts.socket_timeout + assert 10.5 == (get_pool(client)).opts.max_idle_time_seconds + assert 10.5 == client.options.pool_options.max_idle_time_seconds + assert 10.5 == client.options.server_selection_timeout - def test_socket_timeout_ms_validation(self): - c = self.rs_or_single_client(socketTimeoutMS=10 * 1000) - self.assertEqual(10, (get_pool(c)).opts.socket_timeout) + def test_socket_timeout_ms_validation(self, rs_or_single_client): + c = rs_or_single_client(socketTimeoutMS=10 * 1000) + assert 10 == (get_pool(c)).opts.socket_timeout - c = connected(self.rs_or_single_client(socketTimeoutMS=None)) - self.assertEqual(None, (get_pool(c)).opts.socket_timeout) + c = connected(rs_or_single_client(socketTimeoutMS=None)) + assert (get_pool(c)).opts.socket_timeout is None - c = connected(self.rs_or_single_client(socketTimeoutMS=0)) - self.assertEqual(None, (get_pool(c)).opts.socket_timeout) + c = connected(rs_or_single_client(socketTimeoutMS=0)) + assert (get_pool(c)).opts.socket_timeout is None - with self.assertRaises(ValueError): - with self.rs_or_single_client(socketTimeoutMS=-1): + with pytest.raises(ValueError): + with rs_or_single_client(socketTimeoutMS=-1): pass - with self.assertRaises(ValueError): - with self.rs_or_single_client(socketTimeoutMS=1e10): + with pytest.raises(ValueError): + with rs_or_single_client(socketTimeoutMS=1e10): pass - with self.assertRaises(ValueError): - with self.rs_or_single_client(socketTimeoutMS="foo"): + with pytest.raises(ValueError): + with rs_or_single_client(socketTimeoutMS="foo"): pass - def test_socket_timeout(self): - no_timeout = self.client + def test_socket_timeout(self, client_context_fixture, rs_or_single_client): + no_timeout = client_context_fixture.client timeout_sec = 1 - timeout = self.rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) - self.addCleanup(timeout.close) + timeout = rs_or_single_client(socketTimeoutMS=1000 * timeout_sec) no_timeout.pymongo_test.drop_collection("test") no_timeout.pymongo_test.test.insert_one({"x": 1}) @@ -1233,129 +1256,125 @@ def get_x(db): doc = next(db.test.find().where(where_func)) return doc["x"] - self.assertEqual(1, get_x(no_timeout.pymongo_test)) - with self.assertRaises(NetworkTimeout): + assert 1 == get_x(no_timeout.pymongo_test) + with pytest.raises(NetworkTimeout): get_x(timeout.pymongo_test) def test_server_selection_timeout(self): client = MongoClient(serverSelectionTimeoutMS=100, connect=False) - self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0.1) client.close() client = MongoClient(serverSelectionTimeoutMS=0, connect=False) - self.assertAlmostEqual(0, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0) - self.assertRaises(ValueError, MongoClient, serverSelectionTimeoutMS="foo", connect=False) - self.assertRaises(ValueError, MongoClient, serverSelectionTimeoutMS=-1, connect=False) - self.assertRaises( - ConfigurationError, MongoClient, serverSelectionTimeoutMS=None, connect=False - ) + pytest.raises(ValueError, MongoClient, serverSelectionTimeoutMS="foo", connect=False) + pytest.raises(ValueError, MongoClient, serverSelectionTimeoutMS=-1, connect=False) + pytest.raises(ConfigurationError, MongoClient, serverSelectionTimeoutMS=None, connect=False) client.close() client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=100", connect=False) - self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0.1) client.close() client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=0", connect=False) - self.assertAlmostEqual(0, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 0) client.close() # Test invalid timeout in URI ignored and set to default. client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=-1", connect=False) - self.assertAlmostEqual(30, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 30) client.close() client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=", connect=False) - self.assertAlmostEqual(30, client.options.server_selection_timeout) + pytest.approx(client.options.server_selection_timeout, 30) - def test_waitQueueTimeoutMS(self): - client = self.rs_or_single_client(waitQueueTimeoutMS=2000) - self.assertEqual((get_pool(client)).opts.wait_queue_timeout, 2) + def test_waitQueueTimeoutMS(self, rs_or_single_client): + client = rs_or_single_client(waitQueueTimeoutMS=2000) + assert 2 == (get_pool(client)).opts.wait_queue_timeout - def test_socketKeepAlive(self): - pool = get_pool(self.client) + def test_socketKeepAlive(self, client_context_fixture): + pool = get_pool(client_context_fixture.client) with pool.checkout() as conn: keepalive = conn.conn.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE) - self.assertTrue(keepalive) + assert keepalive @no_type_check - def test_tz_aware(self): - self.assertRaises(ValueError, MongoClient, tz_aware="foo") + def test_tz_aware(self, client_context_fixture, rs_or_single_client): + pytest.raises(ValueError, MongoClient, tz_aware="foo") - aware = self.rs_or_single_client(tz_aware=True) - self.addCleanup(aware.close) - naive = self.client + aware = rs_or_single_client(tz_aware=True) + naive = client_context_fixture.client aware.pymongo_test.drop_collection("test") now = datetime.datetime.now(tz=datetime.timezone.utc) aware.pymongo_test.test.insert_one({"x": now}) - self.assertEqual(None, (naive.pymongo_test.test.find_one())["x"].tzinfo) - self.assertEqual(utc, (aware.pymongo_test.test.find_one())["x"].tzinfo) - self.assertEqual( - (aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None), - (naive.pymongo_test.test.find_one())["x"], - ) + assert (naive.pymongo_test.test.find_one())["x"].tzinfo is None + assert utc == (aware.pymongo_test.test.find_one())["x"].tzinfo + assert (aware.pymongo_test.test.find_one())["x"].replace(tzinfo=None) == ( + naive.pymongo_test.test.find_one() + )["x"] - @client_context.require_ipv6 - def test_ipv6(self): - if client_context.tls: + @pytest.mark.usefixtures("require_ipv6") + def test_ipv6(self, client_context_fixture, rs_or_single_client_noauth): + if client_context_fixture.tls: if not HAVE_IPADDRESS: - raise SkipTest("Need the ipaddress module to test with SSL") + pytest.skip("Need the ipaddress module to test with SSL") - if client_context.auth_enabled: + if client_context_fixture.auth_enabled: auth_str = f"{db_user}:{db_pwd}@" else: auth_str = "" - uri = "mongodb://%s[::1]:%d" % (auth_str, client_context.port) - if client_context.is_rs: - uri += "/?replicaSet=" + (client_context.replica_set_name or "") + uri = "mongodb://%s[::1]:%d" % (auth_str, client_context_fixture.port) + if client_context_fixture.is_rs: + uri += "/?replicaSet=" + (client_context_fixture.replica_set_name or "") - client = self.rs_or_single_client_noauth(uri) + client = rs_or_single_client_noauth(uri) client.pymongo_test.test.insert_one({"dummy": "object"}) client.pymongo_test_bernie.test.insert_one({"dummy": "object"}) dbs = client.list_database_names() - self.assertTrue("pymongo_test" in dbs) - self.assertTrue("pymongo_test_bernie" in dbs) + assert "pymongo_test" in dbs + assert "pymongo_test_bernie" in dbs - def test_contextlib(self): - client = self.rs_or_single_client() + def test_contextlib(self, rs_or_single_client): + client = rs_or_single_client() client.pymongo_test.drop_collection("test") client.pymongo_test.test.insert_one({"foo": "bar"}) # The socket used for the previous commands has been returned to the # pool - self.assertEqual(1, len((get_pool(client)).conns)) + assert 1 == len((get_pool(client)).conns) # contextlib async support was added in Python 3.10 if _IS_SYNC or sys.version_info >= (3, 10): with contextlib.closing(client): - self.assertEqual("bar", (client.pymongo_test.test.find_one())["foo"]) - with self.assertRaises(InvalidOperation): + assert "bar" == (client.pymongo_test.test.find_one())["foo"] + with pytest.raises(InvalidOperation): client.pymongo_test.test.find_one() - client = self.rs_or_single_client() + client = rs_or_single_client() with client as client: - self.assertEqual("bar", (client.pymongo_test.test.find_one())["foo"]) - with self.assertRaises(InvalidOperation): + assert "bar" == (client.pymongo_test.test.find_one())["foo"] + with pytest.raises(InvalidOperation): client.pymongo_test.test.find_one() - @client_context.require_sync - def test_interrupt_signal(self): + @pytest.mark.usefixtures("require_sync") + def test_interrupt_signal(self, client_context_fixture): if sys.platform.startswith("java"): # We can't figure out how to raise an exception on a thread that's # blocked on a socket, whether that's the main thread or a worker, # without simply killing the whole thread in Jython. This suggests # PYTHON-294 can't actually occur in Jython. - raise SkipTest("Can't test interrupts in Jython") + pytest.skip("Can't test interrupts in Jython") if is_greenthread_patched(): - raise SkipTest("Can't reliably test interrupts with green threads") + pytest.skip("Can't reliably test interrupts with green threads") # Test fix for PYTHON-294 -- make sure MongoClient closes its # socket if it gets an interrupt while waiting to recv() from it. - db = self.client.pymongo_test + db = client_context_fixture.client.pymongo_test # A $where clause which takes 1.5 sec to execute where = delay(1.5) @@ -1397,48 +1416,48 @@ def sigalarm(num, frame): except KeyboardInterrupt: raised = True - # Can't use self.assertRaises() because it doesn't catch system - # exceptions - self.assertTrue(raised, "Didn't raise expected KeyboardInterrupt") + assert raised, "Didn't raise expected KeyboardInterrupt" # Raises AssertionError due to PYTHON-294 -- Mongo's response to # the previous find() is still waiting to be read on the socket, # so the request id's don't match. - self.assertEqual({"_id": 1}, next(db.foo.find())) # type: ignore[call-overload] + assert {"_id": 1} == next(db.foo.find()) # type: ignore[call-overload] finally: if old_signal_handler: signal.signal(signal.SIGALRM, old_signal_handler) - def test_operation_failure(self): + def test_operation_failure(self, single_client): # Ensure MongoClient doesn't close socket after it gets an error # response to getLastError. PYTHON-395. We need a new client here # to avoid race conditions caused by replica set failover or idle # socket reaping. - client = self.single_client() + client = single_client() client.pymongo_test.test.find_one() pool = get_pool(client) socket_count = len(pool.conns) - self.assertGreaterEqual(socket_count, 1) + assert socket_count >= 1 old_conn = next(iter(pool.conns)) client.pymongo_test.test.drop() client.pymongo_test.test.insert_one({"_id": "foo"}) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): client.pymongo_test.test.insert_one({"_id": "foo"}) - self.assertEqual(socket_count, len(pool.conns)) - new_con = next(iter(pool.conns)) - self.assertEqual(old_conn, new_con) + assert socket_count == len(pool.conns) + new_conn = next(iter(pool.conns)) + assert old_conn == new_conn - def test_lazy_connect_w0(self): + @pytest.mark.parametrize("drop_database_fixture", ["test_lazy_connect_w0"], indirect=True) + def test_lazy_connect_w0( + self, client_context_fixture, rs_or_single_client, drop_database_fixture + ): # Ensure that connect-on-demand works when the first operation is # an unacknowledged write. This exercises _writable_max_wire_version(). # Use a separate collection to avoid races where we're still # completing an operation on a collection while the next test begins. - client_context.client.drop_database("test_lazy_connect_w0") - self.addCleanup(client_context.client.drop_database, "test_lazy_connect_w0") + client_context_fixture.client.drop_database("test_lazy_connect_w0") - client = self.rs_or_single_client(connect=False, w=0) + client = rs_or_single_client(connect=False, w=0) client.test_lazy_connect_w0.test.insert_one({}) def predicate(): @@ -1446,7 +1465,7 @@ def predicate(): wait_until(predicate, "find one document") - client = self.rs_or_single_client(connect=False, w=0) + client = rs_or_single_client(connect=False, w=0) client.test_lazy_connect_w0.test.update_one({}, {"$set": {"x": 1}}) def predicate(): @@ -1454,7 +1473,7 @@ def predicate(): wait_until(predicate, "update one document") - client = self.rs_or_single_client(connect=False, w=0) + client = rs_or_single_client(connect=False, w=0) client.test_lazy_connect_w0.test.delete_one({}) def predicate(): @@ -1462,11 +1481,11 @@ def predicate(): wait_until(predicate, "delete one document") - @client_context.require_no_mongos - def test_exhaust_network_error(self): + @pytest.mark.usefixtures("require_no_mongos") + def test_exhaust_network_error(self, rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid semaphore leaks. - client = self.rs_or_single_client(maxPoolSize=1, retryReads=False) + client = rs_or_single_client(maxPoolSize=1, retryReads=False) collection = client.pymongo_test.test pool = get_pool(client) pool._check_interval_seconds = None # Never check. @@ -1478,23 +1497,21 @@ def test_exhaust_network_error(self): conn = one(pool.conns) conn.conn.close() cursor = collection.find(cursor_type=CursorType.EXHAUST) - with self.assertRaises(ConnectionFailure): + with pytest.raises(ConnectionFailure): next(cursor) - self.assertTrue(conn.closed) + assert conn.closed # The semaphore was decremented despite the error. - self.assertEqual(0, pool.requests) + assert 0 == pool.requests - @client_context.require_auth - def test_auth_network_error(self): + @pytest.mark.usefixtures("require_auth") + def test_auth_network_error(self, rs_or_single_client): # Make sure there's no semaphore leak if we get a network error # when authenticating a new socket with cached credentials. # Get a client with one socket so we detect if it's leaked. - c = connected( - self.rs_or_single_client(maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False) - ) + c = connected(rs_or_single_client(maxPoolSize=1, waitQueueTimeoutMS=1, retryReads=False)) # Cause a network error on the actual socket. pool = get_pool(c) @@ -1503,25 +1520,25 @@ def test_auth_network_error(self): # Connection.authenticate logs, but gets a socket.error. Should be # reraised as AutoReconnect. - with self.assertRaises(AutoReconnect): + with pytest.raises(AutoReconnect): c.test.collection.find_one() # No semaphore leak, the pool is allowed to make a new socket. c.test.collection.find_one() - @client_context.require_no_replica_set - def test_connect_to_standalone_using_replica_set_name(self): - client = self.single_client(replicaSet="anything", serverSelectionTimeoutMS=100) - with self.assertRaises(AutoReconnect): + @pytest.mark.usefixtures("require_no_replica_set") + def test_connect_to_standalone_using_replica_set_name(self, single_client): + client = single_client(replicaSet="anything", serverSelectionTimeoutMS=100) + with pytest.raises(AutoReconnect): client.test.test.find_one() - @client_context.require_replica_set - def test_stale_getmore(self): + @pytest.mark.usefixtures("require_replica_set") + def test_stale_getmore(self, rs_client): # A cursor is created, but its member goes down and is removed from # the topology before the getMore message is sent. Test that # MongoClient._run_operation_with_response handles the error. - with self.assertRaises(AutoReconnect): - client = self.rs_client(connect=False, serverSelectionTimeoutMS=100) + with pytest.raises(AutoReconnect): + client = rs_client(connect=False, serverSelectionTimeoutMS=100) client._run_operation( operation=message._GetMore( "pymongo_test", @@ -1541,7 +1558,7 @@ def test_stale_getmore(self): address=("not-a-member", 27017), ) - def test_heartbeat_frequency_ms(self): + def test_heartbeat_frequency_ms(self, client_context_fixture, single_client): class HeartbeatStartedListener(ServerHeartbeatListener): def __init__(self): self.results = [] @@ -1566,116 +1583,117 @@ def init(self, *args): ServerHeartbeatStartedEvent.__init__ = init # type: ignore listener = HeartbeatStartedListener() uri = "mongodb://%s:%d/?heartbeatFrequencyMS=500" % ( - client_context.host, - client_context.port, + client_context_fixture.host, + client_context_fixture.port, ) - self.single_client(uri, event_listeners=[listener]) + single_client(uri, event_listeners=[listener]) wait_until( lambda: len(listener.results) >= 2, "record two ServerHeartbeatStartedEvents" ) # Default heartbeatFrequencyMS is 10 sec. Check the interval was # closer to 0.5 sec with heartbeatFrequencyMS configured. - self.assertAlmostEqual(heartbeat_times[1] - heartbeat_times[0], 0.5, delta=2) + pytest.approx(heartbeat_times[1] - heartbeat_times[0], 0.5, abs=2) finally: ServerHeartbeatStartedEvent.__init__ = old_init # type: ignore def test_small_heartbeat_frequency_ms(self): uri = "mongodb://example/?heartbeatFrequencyMS=499" - with self.assertRaises(ConfigurationError) as context: + with pytest.raises(ConfigurationError) as context: MongoClient(uri) - self.assertIn("heartbeatFrequencyMS", str(context.exception)) + assert "heartbeatFrequencyMS" in str(context.value) - def test_compression(self): + def test_compression(self, client_context_fixture, simple_client, single_client): def compression_settings(client): pool_options = client.options.pool_options return pool_options._compression_settings - uri = "mongodb://localhost:27017/?compressors=zlib" - client = self.simple_client(uri, connect=False) + client = simple_client("mongodb://localhost:27017/?compressors=zlib", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=4" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + + client = simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=4", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, 4) - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-1" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == 4 + + client = simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-1", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = simple_client("mongodb://localhost:27017", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017/?compressors=foobar" - client = self.simple_client(uri, connect=False) + assert opts.compressors == [] + assert opts.zlib_compression_level == -1 + + client = simple_client("mongodb://localhost:27017/?compressors=foobar", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017/?compressors=foobar,zlib" - client = self.simple_client(uri, connect=False) + assert opts.compressors == [] + assert opts.zlib_compression_level == -1 + + client = simple_client("mongodb://localhost:27017/?compressors=foobar,zlib", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 - # According to the connection string spec, unsupported values - # just raise a warning and are ignored. - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=10" - client = self.simple_client(uri, connect=False) + client = simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=10", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) - uri = "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-2" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 + + client = simple_client( + "mongodb://localhost:27017/?compressors=zlib&zlibCompressionLevel=-2", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zlib"]) - self.assertEqual(opts.zlib_compression_level, -1) + assert opts.compressors == ["zlib"] + assert opts.zlib_compression_level == -1 if not _have_snappy(): - uri = "mongodb://localhost:27017/?compressors=snappy" - client = self.simple_client(uri, connect=False) + client = simple_client("mongodb://localhost:27017/?compressors=snappy", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) + assert opts.compressors == [] else: - uri = "mongodb://localhost:27017/?compressors=snappy" - client = self.simple_client(uri, connect=False) + client = simple_client("mongodb://localhost:27017/?compressors=snappy", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["snappy"]) - uri = "mongodb://localhost:27017/?compressors=snappy,zlib" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["snappy"] + client = simple_client( + "mongodb://localhost:27017/?compressors=snappy,zlib", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["snappy", "zlib"]) + assert opts.compressors == ["snappy", "zlib"] if not _have_zstd(): - uri = "mongodb://localhost:27017/?compressors=zstd" - client = self.simple_client(uri, connect=False) + client = simple_client("mongodb://localhost:27017/?compressors=zstd", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, []) + assert opts.compressors == [] else: - uri = "mongodb://localhost:27017/?compressors=zstd" - client = self.simple_client(uri, connect=False) + client = simple_client("mongodb://localhost:27017/?compressors=zstd", connect=False) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zstd"]) - uri = "mongodb://localhost:27017/?compressors=zstd,zlib" - client = self.simple_client(uri, connect=False) + assert opts.compressors == ["zstd"] + client = simple_client( + "mongodb://localhost:27017/?compressors=zstd,zlib", connect=False + ) opts = compression_settings(client) - self.assertEqual(opts.compressors, ["zstd", "zlib"]) + assert opts.compressors == ["zstd", "zlib"] - options = client_context.default_client_options + options = client_context_fixture.default_client_options if "compressors" in options and "zlib" in options["compressors"]: for level in range(-1, 10): - client = self.single_client(zlibcompressionlevel=level) - # No error - client.pymongo_test.test.find_one() + client = single_client(zlibcompressionlevel=level) + client.pymongo_test.test.find_one() # No error - @client_context.require_sync - def test_reset_during_update_pool(self): - client = self.rs_or_single_client(minPoolSize=10) + @pytest.mark.usefixtures("require_sync") + def test_reset_during_update_pool(self, rs_or_single_client): + client = rs_or_single_client(minPoolSize=10) client.admin.command("ping") pool = get_pool(client) generation = pool.gen.get_overall() @@ -1703,8 +1721,7 @@ def run(self): t = ResetPoolThread(pool) t.start() - # Ensure that update_pool completes without error even when the pool - # is reset concurrently. + # Ensure that update_pool completes without error even when the pool is reset concurrently. try: while True: for _ in range(10): @@ -1716,15 +1733,14 @@ def run(self): t.join() client.admin.command("ping") - def test_background_connections_do_not_hold_locks(self): + def test_background_connections_do_not_hold_locks(self, rs_or_single_client): min_pool_size = 10 - client = self.rs_or_single_client( + client = rs_or_single_client( serverSelectionTimeoutMS=3000, minPoolSize=min_pool_size, connect=False ) - # Create a single connection in the pool. - client.admin.command("ping") + client.admin.command("ping") # Create a single connection in the pool - # Cause new connections stall for a few seconds. + # Cause new connections to stall for a few seconds. pool = get_pool(client) original_connect = pool.connect @@ -1732,44 +1748,39 @@ def stall_connect(*args, **kwargs): time.sleep(2) return original_connect(*args, **kwargs) - pool.connect = stall_connect - # Un-patch Pool.connect to break the cyclic reference. - self.addCleanup(delattr, pool, "connect") - - # Wait for the background thread to start creating connections - wait_until(lambda: len(pool.conns) > 1, "start creating connections") + try: + pool.connect = stall_connect + + wait_until(lambda: len(pool.conns) > 1, "start creating connections") + # Assert that application operations do not block. + for _ in range(10): + start = time.monotonic() + client.admin.command("ping") + total = time.monotonic() - start + assert total < 2 + finally: + delattr(pool, "connect") - # Assert that application operations do not block. - for _ in range(10): - start = time.monotonic() - client.admin.command("ping") - total = time.monotonic() - start - # Each ping command should not take more than 2 seconds - self.assertLess(total, 2) - - @client_context.require_replica_set - def test_direct_connection(self): - # direct_connection=True should result in Single topology. - client = self.rs_or_single_client(directConnection=True) + @pytest.mark.usefixtures("require_replica_set") + def test_direct_connection(self, rs_or_single_client): + client = rs_or_single_client(directConnection=True) client.admin.command("ping") - self.assertEqual(len(client.nodes), 1) - self.assertEqual(client._topology_settings.get_topology_type(), TOPOLOGY_TYPE.Single) + assert len(client.nodes) == 1 + assert client._topology_settings.get_topology_type() == TOPOLOGY_TYPE.Single - # direct_connection=False should result in RS topology. - client = self.rs_or_single_client(directConnection=False) + client = rs_or_single_client(directConnection=False) client.admin.command("ping") - self.assertGreaterEqual(len(client.nodes), 1) - self.assertIn( - client._topology_settings.get_topology_type(), - [TOPOLOGY_TYPE.ReplicaSetNoPrimary, TOPOLOGY_TYPE.ReplicaSetWithPrimary], - ) + assert len(client.nodes) >= 1 + assert client._topology_settings.get_topology_type() in [ + TOPOLOGY_TYPE.ReplicaSetNoPrimary, + TOPOLOGY_TYPE.ReplicaSetWithPrimary, + ] - # directConnection=True, should error with multiple hosts as a list. - with self.assertRaises(ConfigurationError): + with pytest.raises(ConfigurationError): MongoClient(["host1", "host2"], directConnection=True) - @unittest.skipIf("PyPy" in sys.version, "PYTHON-2927 fails often on PyPy") - def test_continuous_network_errors(self): + @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2927 fails often on PyPy") + def test_continuous_network_errors(self, simple_client): def server_description_count(): i = 0 for obj in gc.get_objects(): @@ -1782,50 +1793,43 @@ def server_description_count(): gc.collect() with client_knobs(min_heartbeat_interval=0.003): - client = self.simple_client( + client = simple_client( "invalid:27017", heartbeatFrequencyMS=3, serverSelectionTimeoutMS=150 ) initial_count = server_description_count() - with self.assertRaises(ServerSelectionTimeoutError): + with pytest.raises(ServerSelectionTimeoutError): client.test.test.find_one() gc.collect() final_count = server_description_count() - # If a bug like PYTHON-2433 is reintroduced then too many - # ServerDescriptions will be kept alive and this test will fail: - # AssertionError: 19 != 46 within 15 delta (27 difference) - # On Python 3.11 we seem to get more of a delta. - self.assertAlmostEqual(initial_count, final_count, delta=20) - - @client_context.require_failCommand_fail_point - def test_network_error_message(self): - client = self.single_client(retryReads=False) + assert pytest.approx(initial_count, abs=20) == final_count + + @pytest.mark.usefixtures("require_failCommand_fail_point") + def test_network_error_message(self, single_client): + client = single_client(retryReads=False) client.admin.command("ping") # connect with self.fail_point( - {"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}} + client, + {"mode": {"times": 1}, "data": {"closeConnection": True, "failCommands": ["find"]}}, ): assert client.address is not None expected = "{}:{}: ".format(*(client.address)) - with self.assertRaisesRegex(AutoReconnect, expected): + with pytest.raises(AutoReconnect, match=expected): client.pymongo_test.test.find_one({}) - @unittest.skipIf("PyPy" in sys.version, "PYTHON-2938 could fail on PyPy") - def test_process_periodic_tasks(self): - client = self.rs_or_single_client() + @pytest.mark.skipif("PyPy" in sys.version, reason="PYTHON-2938 could fail on PyPy") + def test_process_periodic_tasks(self, rs_or_single_client): + client = rs_or_single_client() coll = client.db.collection coll.insert_many([{} for _ in range(5)]) cursor = coll.find(batch_size=2) cursor.next() c_id = cursor.cursor_id - self.assertIsNotNone(c_id) + assert c_id is not None client.close() - # Add cursor to kill cursors queue del cursor - wait_until( - lambda: client._kill_cursors_queue, - "waited for cursor to be added to queue", - ) + wait_until(lambda: client._kill_cursors_queue, "waited for cursor to be added to queue") client._process_periodic_tasks() # This must not raise or print any exceptions - with self.assertRaises(InvalidOperation): + with pytest.raises(InvalidOperation): coll.insert_many([{} for _ in range(5)]) def test_service_name_from_kwargs(self): @@ -1834,82 +1838,79 @@ def test_service_name_from_kwargs(self): srvServiceName="customname", connect=False, ) - self.assertEqual(client._topology_settings.srv_service_name, "customname") + assert client._topology_settings.srv_service_name == "customname" + client = MongoClient( - "mongodb+srv://user:password@test22.test.build.10gen.cc" - "/?srvServiceName=shouldbeoverriden", + "mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=shouldbeoverriden", srvServiceName="customname", connect=False, ) - self.assertEqual(client._topology_settings.srv_service_name, "customname") + assert client._topology_settings.srv_service_name == "customname" + client = MongoClient( "mongodb+srv://user:password@test22.test.build.10gen.cc/?srvServiceName=customname", connect=False, ) - self.assertEqual(client._topology_settings.srv_service_name, "customname") - - def test_srv_max_hosts_kwarg(self): - client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/") - self.assertGreater(len(client.topology_description.server_descriptions()), 1) - client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1) - self.assertEqual(len(client.topology_description.server_descriptions()), 1) - client = self.simple_client( + assert client._topology_settings.srv_service_name == "customname" + + def test_srv_max_hosts_kwarg(self, simple_client): + client = simple_client("mongodb+srv://test1.test.build.10gen.cc/") + assert len(client.topology_description.server_descriptions()) > 1 + + client = simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1) + assert len(client.topology_description.server_descriptions()) == 1 + + client = simple_client( "mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1", srvmaxhosts=2 ) - self.assertEqual(len(client.topology_description.server_descriptions()), 2) - - @unittest.skipIf( - client_context.load_balancer or client_context.serverless, - "loadBalanced clients do not run SDAM", - ) - @unittest.skipIf(sys.platform == "win32", "Windows does not support SIGSTOP") - @client_context.require_sync - def test_sigstop_sigcont(self): + assert len(client.topology_description.server_descriptions()) == 2 + + @pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support SIGSTOP") + @pytest.mark.usefixtures("require_sdam") + @pytest.mark.usefixtures("require_sync") + def test_sigstop_sigcont(self, client_context_fixture): test_dir = os.path.dirname(os.path.realpath(__file__)) script = os.path.join(test_dir, "sigstop_sigcont.py") - p = subprocess.Popen( - [sys.executable, script, client_context.uri], + with subprocess.Popen( + [sys.executable, script, client_context_fixture.uri], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - self.addCleanup(p.wait, timeout=1) - self.addCleanup(p.kill) - time.sleep(1) - # Stop the child, sleep for twice the streaming timeout - # (heartbeatFrequencyMS + connectTimeoutMS), and restart. - os.kill(p.pid, signal.SIGSTOP) - time.sleep(2) - os.kill(p.pid, signal.SIGCONT) - time.sleep(0.5) - # Tell the script to exit gracefully. - outs, _ = p.communicate(input=b"q\n", timeout=10) - self.assertTrue(outs) - log_output = outs.decode("utf-8") - self.assertIn("TEST STARTED", log_output) - self.assertIn("ServerHeartbeatStartedEvent", log_output) - self.assertIn("ServerHeartbeatSucceededEvent", log_output) - self.assertIn("TEST COMPLETED", log_output) - self.assertNotIn("ServerHeartbeatFailedEvent", log_output) - - def _test_handshake(self, env_vars, expected_env): + ) as p: + time.sleep(1) + os.kill(p.pid, signal.SIGSTOP) + time.sleep(2) + os.kill(p.pid, signal.SIGCONT) + time.sleep(0.5) + outs, _ = p.communicate(input=b"q\n", timeout=10) + assert outs + log_output = outs.decode("utf-8") + assert "TEST STARTED" in log_output + assert "ServerHeartbeatStartedEvent" in log_output + assert "ServerHeartbeatSucceededEvent" in log_output + assert "TEST COMPLETED" in log_output + assert "ServerHeartbeatFailedEvent" not in log_output + + def _test_handshake(self, env_vars, expected_env, rs_or_single_client): with patch.dict("os.environ", env_vars): metadata = copy.deepcopy(_METADATA) if has_c(): metadata["driver"]["name"] = "PyMongo|c" else: metadata["driver"]["name"] = "PyMongo" + if expected_env is not None: metadata["env"] = expected_env if "AWS_REGION" not in env_vars: os.environ["AWS_REGION"] = "" - client = self.rs_or_single_client(serverSelectionTimeoutMS=10000) + + client = rs_or_single_client(serverSelectionTimeoutMS=10000) client.admin.command("ping") options = client.options - self.assertEqual(options.pool_options.metadata, metadata) + assert options.pool_options.metadata == metadata - def test_handshake_01_aws(self): + def test_handshake_01_aws(self, rs_or_single_client): self._test_handshake( { "AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", @@ -1917,12 +1918,18 @@ def test_handshake_01_aws(self): "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", }, {"name": "aws.lambda", "region": "us-east-2", "memory_mb": 1024}, + rs_or_single_client, ) - def test_handshake_02_azure(self): - self._test_handshake({"FUNCTIONS_WORKER_RUNTIME": "python"}, {"name": "azure.func"}) + def test_handshake_02_azure(self, rs_or_single_client): + self._test_handshake( + {"FUNCTIONS_WORKER_RUNTIME": "python"}, + {"name": "azure.func"}, + rs_or_single_client, + ) - def test_handshake_03_gcp(self): + def test_handshake_03_gcp(self, rs_or_single_client): + # Regular case with environment variables. self._test_handshake( { "K_SERVICE": "servicename", @@ -1931,7 +1938,9 @@ def test_handshake_03_gcp(self): "FUNCTION_REGION": "us-central1", }, {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, + rs_or_single_client, ) + # Extra case for FUNCTION_NAME. self._test_handshake( { @@ -1941,45 +1950,50 @@ def test_handshake_03_gcp(self): "FUNCTION_REGION": "us-central1", }, {"name": "gcp.func", "region": "us-central1", "memory_mb": 1024, "timeout_sec": 60}, + rs_or_single_client, ) - def test_handshake_04_vercel(self): + def test_handshake_04_vercel(self, rs_or_single_client): self._test_handshake( - {"VERCEL": "1", "VERCEL_REGION": "cdg1"}, {"name": "vercel", "region": "cdg1"} + {"VERCEL": "1", "VERCEL_REGION": "cdg1"}, + {"name": "vercel", "region": "cdg1"}, + rs_or_single_client, ) - def test_handshake_05_multiple(self): + def test_handshake_05_multiple(self, rs_or_single_client): self._test_handshake( {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "FUNCTIONS_WORKER_RUNTIME": "python"}, None, + rs_or_single_client, ) - # Extra cases for other combos. + self._test_handshake( {"FUNCTIONS_WORKER_RUNTIME": "python", "K_SERVICE": "servicename"}, None, + rs_or_single_client, ) - self._test_handshake({"K_SERVICE": "servicename", "VERCEL": "1"}, None) - def test_handshake_06_region_too_long(self): + self._test_handshake({"K_SERVICE": "servicename", "VERCEL": "1"}, None, rs_or_single_client) + + def test_handshake_06_region_too_long(self, rs_or_single_client): self._test_handshake( {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_REGION": "a" * 512}, {"name": "aws.lambda"}, + rs_or_single_client, ) - def test_handshake_07_memory_invalid_int(self): + def test_handshake_07_memory_invalid_int(self, rs_or_single_client): self._test_handshake( {"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big"}, {"name": "aws.lambda"}, + rs_or_single_client, ) - def test_handshake_08_invalid_aws_ec2(self): + def test_handshake_08_invalid_aws_ec2(self, rs_or_single_client): # AWS_EXECUTION_ENV needs to start with "AWS_Lambda_". - self._test_handshake( - {"AWS_EXECUTION_ENV": "EC2"}, - None, - ) + self._test_handshake({"AWS_EXECUTION_ENV": "EC2"}, None, rs_or_single_client) - def test_handshake_09_container_with_provider(self): + def test_handshake_09_container_with_provider(self, rs_or_single_client): self._test_handshake( { ENV_VAR_K8S: "1", @@ -1993,102 +2007,96 @@ def test_handshake_09_container_with_provider(self): "region": "us-east-1", "memory_mb": 256, }, + rs_or_single_client, ) - def test_dict_hints(self): - self.db.t.find(hint={"x": 1}) + def test_dict_hints(self, client_context_fixture): + client_context_fixture.client.db.t.find(hint={"x": 1}) - def test_dict_hints_sort(self): - result = self.db.t.find() + def test_dict_hints_sort(self, client_context_fixture): + result = client_context_fixture.client.db.t.find() result.sort({"x": 1}) + client_context_fixture.client.db.t.find(sort={"x": 1}) - self.db.t.find(sort={"x": 1}) - - def test_dict_hints_create_index(self): - self.db.t.create_index({"x": pymongo.ASCENDING}) + def test_dict_hints_create_index(self, client_context_fixture): + client_context_fixture.client.db.t.create_index({"x": pymongo.ASCENDING}) - def test_legacy_java_uuid_roundtrip(self): + def test_legacy_java_uuid_roundtrip(self, client_context_fixture): data = BinaryData.java_data docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, JAVA_LEGACY)) - client_context.client.pymongo_test.drop_collection("java_uuid") - db = client_context.client.pymongo_test + client_context_fixture.client.pymongo_test.drop_collection("java_uuid") + db = client_context_fixture.client.pymongo_test coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=JAVA_LEGACY)) coll.insert_many(docs) - self.assertEqual(5, coll.count_documents({})) + assert coll.count_documents({}) == 5 for d in coll.find(): - self.assertEqual(d["newguid"], uuid.UUID(d["newguidstring"])) + assert d["newguid"] == uuid.UUID(d["newguidstring"]) coll = db.get_collection("java_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) for d in coll.find(): - self.assertNotEqual(d["newguid"], d["newguidstring"]) - client_context.client.pymongo_test.drop_collection("java_uuid") + assert d["newguid"] != d["newguidstring"] + client_context_fixture.client.pymongo_test.drop_collection("java_uuid") - def test_legacy_csharp_uuid_roundtrip(self): + def test_legacy_csharp_uuid_roundtrip(self, client_context_fixture): data = BinaryData.csharp_data docs = bson.decode_all(data, CodecOptions(SON[str, Any], False, CSHARP_LEGACY)) - client_context.client.pymongo_test.drop_collection("csharp_uuid") - db = client_context.client.pymongo_test + client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") + db = client_context_fixture.client.pymongo_test coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=CSHARP_LEGACY)) coll.insert_many(docs) - self.assertEqual(5, coll.count_documents({})) + assert coll.count_documents({}) == 5 for d in coll.find(): - self.assertEqual(d["newguid"], uuid.UUID(d["newguidstring"])) + assert d["newguid"] == uuid.UUID(d["newguidstring"]) coll = db.get_collection("csharp_uuid", CodecOptions(uuid_representation=PYTHON_LEGACY)) for d in coll.find(): - self.assertNotEqual(d["newguid"], d["newguidstring"]) - client_context.client.pymongo_test.drop_collection("csharp_uuid") + assert d["newguid"] != d["newguidstring"] + client_context_fixture.client.pymongo_test.drop_collection("csharp_uuid") - def test_uri_to_uuid(self): + def test_uri_to_uuid(self, single_client): uri = "mongodb://foo/?uuidrepresentation=csharpLegacy" - client = self.single_client(uri, connect=False) - self.assertEqual(client.pymongo_test.test.codec_options.uuid_representation, CSHARP_LEGACY) + client = single_client(uri, connect=False) + assert client.pymongo_test.test.codec_options.uuid_representation == CSHARP_LEGACY - def test_uuid_queries(self): - db = client_context.client.pymongo_test + def test_uuid_queries(self, client_context_fixture): + db = client_context_fixture.client.pymongo_test coll = db.test coll.drop() uu = uuid.uuid4() coll.insert_one({"uuid": Binary(uu.bytes, 3)}) - self.assertEqual(1, coll.count_documents({})) + assert coll.count_documents({}) == 1 - # Test regular UUID queries (using subtype 4). coll = db.get_collection( "test", CodecOptions(uuid_representation=UuidRepresentation.STANDARD) ) - self.assertEqual(0, coll.count_documents({"uuid": uu})) + assert coll.count_documents({"uuid": uu}) == 0 coll.insert_one({"uuid": uu}) - self.assertEqual(2, coll.count_documents({})) - docs = coll.find({"uuid": uu}).to_list() - self.assertEqual(1, len(docs)) - self.assertEqual(uu, docs[0]["uuid"]) + assert coll.count_documents({}) == 2 + docs = coll.find({"uuid": uu}).to_list(length=1) + assert len(docs) == 1 + assert docs[0]["uuid"] == uu - # Test both. uu_legacy = Binary.from_uuid(uu, UuidRepresentation.PYTHON_LEGACY) predicate = {"uuid": {"$in": [uu, uu_legacy]}} - self.assertEqual(2, coll.count_documents(predicate)) - docs = coll.find(predicate).to_list() - self.assertEqual(2, len(docs)) + assert coll.count_documents(predicate) == 2 + docs = coll.find(predicate).to_list(length=2) + assert len(docs) == 2 coll.drop() -class TestExhaustCursor(IntegrationTest): - """Test that clients properly handle errors from exhaust cursors.""" - - def setUp(self): - super().setUp() - if client_context.is_mongos: - raise SkipTest("mongos doesn't support exhaust, SERVER-2627") - - def test_exhaust_query_server_error(self): +@pytest.mark.usefixtures("require_no_mongos") +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestExhaustCursor(PyMongoTestCasePyTest): + def test_exhaust_query_server_error(self, rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid semaphore leaks. - client = connected(self.rs_or_single_client(maxPoolSize=1)) + client = connected(rs_or_single_client(maxPoolSize=1)) collection = client.pymongo_test.test pool = get_pool(client) @@ -2100,23 +2108,22 @@ def test_exhaust_query_server_error(self): SON([("$query", {}), ("$orderby", True)]), cursor_type=CursorType.EXHAUST ) - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): cursor.next() - self.assertFalse(conn.closed) + assert not conn.closed # The socket was checked in and the semaphore was decremented. - self.assertIn(conn, pool.conns) - self.assertEqual(0, pool.requests) + assert conn in pool.conns + assert pool.requests == 0 - def test_exhaust_getmore_server_error(self): + def test_exhaust_getmore_server_error(self, rs_or_single_client): # When doing a getmore on an exhaust cursor, the socket stays checked # out on success but it's checked in on error to avoid semaphore leaks. - client = self.rs_or_single_client(maxPoolSize=1) + client = rs_or_single_client(maxPoolSize=1) collection = client.pymongo_test.test collection.drop() collection.insert_many([{} for _ in range(200)]) - self.addCleanup(client_context.client.pymongo_test.test.drop) pool = get_pool(client) pool._check_interval_seconds = None # Never check. @@ -2138,19 +2145,19 @@ def receive_message(request_id): return message._OpReply.unpack(msg) conn.receive_message = receive_message - with self.assertRaises(OperationFailure): + with pytest.raises(OperationFailure): cursor.to_list() # Unpatch the instance. del conn.receive_message # The socket is returned to the pool and it still works. - self.assertEqual(200, collection.count_documents({})) - self.assertIn(conn, pool.conns) + assert 200 == collection.count_documents({}) + assert conn in pool.conns - def test_exhaust_query_network_error(self): + def test_exhaust_query_network_error(self, rs_or_single_client): # When doing an exhaust query, the socket stays checked out on success # but must be checked in on error to avoid semaphore leaks. - client = connected(self.rs_or_single_client(maxPoolSize=1, retryReads=False)) + client = connected(rs_or_single_client(maxPoolSize=1, retryReads=False)) collection = client.pymongo_test.test pool = get_pool(client) pool._check_interval_seconds = None # Never check. @@ -2160,18 +2167,18 @@ def test_exhaust_query_network_error(self): conn.conn.close() cursor = collection.find(cursor_type=CursorType.EXHAUST) - with self.assertRaises(ConnectionFailure): + with pytest.raises(ConnectionFailure): cursor.next() - self.assertTrue(conn.closed) + assert conn.closed # The socket was closed and the semaphore was decremented. - self.assertNotIn(conn, pool.conns) - self.assertEqual(0, pool.requests) + assert conn not in pool.conns + assert 0 == pool.requests - def test_exhaust_getmore_network_error(self): + def test_exhaust_getmore_network_error(self, rs_or_single_client): # When doing a getmore on an exhaust cursor, the socket stays checked # out on success but it's checked in on error to avoid semaphore leaks. - client = self.rs_or_single_client(maxPoolSize=1) + client = rs_or_single_client(maxPoolSize=1) collection = client.pymongo_test.test collection.drop() collection.insert_many([{} for _ in range(200)]) # More than one batch. @@ -2188,39 +2195,39 @@ def test_exhaust_getmore_network_error(self): conn.conn.close() # A getmore fails. - with self.assertRaises(ConnectionFailure): + with pytest.raises(ConnectionFailure): cursor.to_list() - self.assertTrue(conn.closed) + assert conn.closed wait_until( lambda: len(client._kill_cursors_queue) == 0, "waited for all killCursor requests to complete", ) # The socket was closed and the semaphore was decremented. - self.assertNotIn(conn, pool.conns) - self.assertEqual(0, pool.requests) + assert conn not in pool.conns + assert 0 == pool.requests - @client_context.require_sync - def test_gevent_task(self): + @pytest.mark.usefixtures("require_sync") + def test_gevent_task(self, client_context_fixture): if not gevent_monkey_patched(): - raise SkipTest("Must be running monkey patched by gevent") + pytest.skip("Must be running monkey patched by gevent") from gevent import spawn def poller(): while True: - client_context.client.pymongo_test.test.insert_one({}) + client_context_fixture.client.pymongo_test.test.insert_one({}) task = spawn(poller) task.kill() - self.assertTrue(task.dead) + assert task.dead - @client_context.require_sync - def test_gevent_timeout(self): + @pytest.mark.usefixtures("require_sync") + def test_gevent_timeout(self, rs_or_single_client): if not gevent_monkey_patched(): - raise SkipTest("Must be running monkey patched by gevent") + pytest.skip("Must be running monkey patched by gevent") from gevent import Timeout, spawn - client = self.rs_or_single_client(maxPoolSize=1) + client = rs_or_single_client(maxPoolSize=1) coll = client.pymongo_test.test coll.insert_one({}) @@ -2241,19 +2248,19 @@ def timeout_task(): tt = spawn(timeout_task) tt.join(15) ct.join(15) - self.assertTrue(tt.dead) - self.assertTrue(ct.dead) - self.assertIsNone(tt.get()) - self.assertIsNone(ct.get()) + assert tt.dead + assert ct.dead + assert tt.get() is None + assert ct.get() is None - @client_context.require_sync - def test_gevent_timeout_when_creating_connection(self): + @pytest.mark.usefixtures("require_sync") + def test_gevent_timeout_when_creating_connection(self, rs_or_single_client): if not gevent_monkey_patched(): - raise SkipTest("Must be running monkey patched by gevent") + pytest.skip("Must be running monkey patched by gevent") from gevent import Timeout, spawn - client = self.rs_or_single_client() - self.addCleanup(client.close) + client = rs_or_single_client() + coll = client.pymongo_test.test pool = get_pool(client) @@ -2276,23 +2283,26 @@ def timeout_task(): tt.join(10) # Assert that we got our active_sockets count back - self.assertEqual(pool.active_sockets, 0) + assert pool.active_sockets == 0 # Assert the greenlet is dead - self.assertTrue(tt.dead) + assert tt.dead # Assert that the Timeout was raised all the way to the try - self.assertTrue(tt.get()) + assert tt.get() # Unpatch the instance. del pool.connect -class TestClientLazyConnect(IntegrationTest): +@pytest.mark.usefixtures("require_sync") +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestClientLazyConnect: """Test concurrent operations on a lazily-connecting MongoClient.""" - def _get_client(self): - return self.rs_or_single_client(connect=False) + @pytest.fixture + def _get_client(self, rs_or_single_client): + return rs_or_single_client(connect=False) - @client_context.require_sync - def test_insert_one(self): + def test_insert_one(self, _get_client, client_context_fixture): def reset(collection): collection.drop() @@ -2300,12 +2310,11 @@ def insert_one(collection, _): collection.insert_one({}) def test(collection): - self.assertEqual(NTHREADS, collection.count_documents({})) + assert NTHREADS == collection.count_documents({}) - lazy_client_trial(reset, insert_one, test, self._get_client) + lazy_client_trial(reset, insert_one, test, _get_client, client_context_fixture) - @client_context.require_sync - def test_update_one(self): + def test_update_one(self, _get_client, client_context_fixture): def reset(collection): collection.drop() collection.insert_one({"i": 0}) @@ -2315,12 +2324,11 @@ def update_one(collection, _): collection.update_one({}, {"$inc": {"i": 1}}) def test(collection): - self.assertEqual(NTHREADS, collection.find_one()["i"]) + assert NTHREADS == collection.find_one()["i"] - lazy_client_trial(reset, update_one, test, self._get_client) + lazy_client_trial(reset, update_one, test, _get_client, client_context_fixture) - @client_context.require_sync - def test_delete_one(self): + def test_delete_one(self, _get_client, client_context_fixture): def reset(collection): collection.drop() collection.insert_many([{"i": i} for i in range(NTHREADS)]) @@ -2329,12 +2337,11 @@ def delete_one(collection, i): collection.delete_one({"i": i}) def test(collection): - self.assertEqual(0, collection.count_documents({})) + assert 0 == collection.count_documents({}) - lazy_client_trial(reset, delete_one, test, self._get_client) + lazy_client_trial(reset, delete_one, test, _get_client, client_context_fixture) - @client_context.require_sync - def test_find_one(self): + def test_find_one(self, _get_client, client_context_fixture): results: list = [] def reset(collection): @@ -2346,14 +2353,23 @@ def find_one(collection, _): results.append(collection.find_one()) def test(collection): - self.assertEqual(NTHREADS, len(results)) + assert NTHREADS == len(results) + + lazy_client_trial(reset, find_one, test, _get_client, client_context_fixture) - lazy_client_trial(reset, find_one, test, self._get_client) +@pytest.mark.usefixtures("require_no_load_balancer") +@pytest.mark.unit +class TestMongoClientFailover: + @pytest.fixture(scope="class", autouse=True) + def _client_knobs(self): + knobs = client_knobs(heartbeat_frequency=0.001, min_heartbeat_interval=0.001) + knobs.enable() + yield knobs + knobs.disable() -class TestMongoClientFailover(MockClientTest): - def test_discover_primary(self): - c = MockClient.get_mock_client( + def test_discover_primary(self, mock_client): + c = mock_client( standalones=[], members=["a:1", "b:2", "c:3"], mongoses=[], @@ -2361,11 +2377,10 @@ def test_discover_primary(self): replicaSet="rs", heartbeatFrequencyMS=500, ) - self.addCleanup(c.close) wait_until(lambda: len(c.nodes) == 3, "connect") - self.assertEqual(c.address, ("a", 1)) + assert c.address == ("a", 1) # Fail over. c.kill_host("a:1") c.mock_primary = "b:2" @@ -2375,11 +2390,11 @@ def predicate(): wait_until(predicate, "wait for server address to be updated") # a:1 not longer in nodes. - self.assertLess(len(c.nodes), 3) + assert len(c.nodes) < 3 - def test_reconnect(self): + def test_reconnect(self, mock_client): # Verify the node list isn't forgotten during a network failure. - c = MockClient.get_mock_client( + c = mock_client( standalones=[], members=["a:1", "b:2", "c:3"], mongoses=[], @@ -2388,7 +2403,6 @@ def test_reconnect(self): retryReads=False, serverSelectionTimeoutMS=1000, ) - self.addCleanup(c.close) wait_until(lambda: len(c.nodes) == 3, "connect") @@ -2400,7 +2414,7 @@ def test_reconnect(self): # MongoClient discovers it's alone. The first attempt raises either # ServerSelectionTimeoutError or AutoReconnect (from # AsyncMockPool.get_socket). - with self.assertRaises(AutoReconnect): + with pytest.raises(AutoReconnect): c.db.collection.find_one() # But it can reconnect. @@ -2408,14 +2422,14 @@ def test_reconnect(self): (c._get_topology()).select_servers( writable_server_selector, _Op.TEST, server_selection_timeout=10 ) - self.assertEqual(c.address, ("a", 1)) + assert c.address == ("a", 1) - def _test_network_error(self, operation_callback): + def _test_network_error(self, mock_client, operation_callback): # Verify only the disconnected server is reset by a network failure. # Disable background refresh. with client_knobs(heartbeat_frequency=999999): - c = MockClient( + c = mock_client( standalones=[], members=["a:1", "b:2"], mongoses=[], @@ -2426,8 +2440,6 @@ def _test_network_error(self, operation_callback): serverSelectionTimeoutMS=1000, ) - self.addCleanup(c.close) - # Set host-specific information so we can test whether it is reset. c.set_wire_version_range("a:1", 2, MIN_SUPPORTED_WIRE_VERSION) c.set_wire_version_range("b:2", 2, MIN_SUPPORTED_WIRE_VERSION + 1) @@ -2439,59 +2451,60 @@ def _test_network_error(self, operation_callback): # MongoClient is disconnected from the primary. This raises either # ServerSelectionTimeoutError or AutoReconnect (from # MockPool.get_socket). - with self.assertRaises(AutoReconnect): + with pytest.raises(AutoReconnect): operation_callback(c) # The primary's description is reset. server_a = (c._get_topology()).get_server_by_address(("a", 1)) sd_a = server_a.description - self.assertEqual(SERVER_TYPE.Unknown, sd_a.server_type) - self.assertEqual(0, sd_a.min_wire_version) - self.assertEqual(0, sd_a.max_wire_version) + assert SERVER_TYPE.Unknown == sd_a.server_type + assert 0 == sd_a.min_wire_version + assert 0 == sd_a.max_wire_version # ...but not the secondary's. server_b = (c._get_topology()).get_server_by_address(("b", 2)) sd_b = server_b.description - self.assertEqual(SERVER_TYPE.RSSecondary, sd_b.server_type) - self.assertEqual(2, sd_b.min_wire_version) - self.assertEqual(MIN_SUPPORTED_WIRE_VERSION + 1, sd_b.max_wire_version) + assert sd_b.server_type == SERVER_TYPE.RSSecondary + assert sd_b.min_wire_version == 2 + assert sd_b.max_wire_version == MIN_SUPPORTED_WIRE_VERSION + 1 - def test_network_error_on_query(self): + def test_network_error_on_query(self, mock_client): def callback(client): return client.db.collection.find_one() - self._test_network_error(callback) + self._test_network_error(mock_client, callback) - def test_network_error_on_insert(self): + def test_network_error_on_insert(self, mock_client): def callback(client): return client.db.collection.insert_one({}) - self._test_network_error(callback) + self._test_network_error(mock_client, callback) - def test_network_error_on_update(self): + def test_network_error_on_update(self, mock_client): def callback(client): return client.db.collection.update_one({}, {"$unset": "x"}) - self._test_network_error(callback) + self._test_network_error(mock_client, callback) - def test_network_error_on_replace(self): + def test_network_error_on_replace(self, mock_client): def callback(client): return client.db.collection.replace_one({}, {}) - self._test_network_error(callback) + self._test_network_error(mock_client, callback) - def test_network_error_on_delete(self): + def test_network_error_on_delete(self, mock_client): def callback(client): return client.db.collection.delete_many({}) - self._test_network_error(callback) + self._test_network_error(mock_client, callback) -class TestClientPool(MockClientTest): - @client_context.require_connection - def test_rs_client_does_not_maintain_pool_to_arbiters(self): +@pytest.mark.usefixtures("require_integration") +@pytest.mark.integration +class TestClientPool: + def test_rs_client_does_not_maintain_pool_to_arbiters(self, mock_client): listener = CMAPListener() - c = MockClient.get_mock_client( + c = mock_client( standalones=[], members=["a:1", "b:2", "c:3", "d:4"], mongoses=[], @@ -2502,27 +2515,21 @@ def test_rs_client_does_not_maintain_pool_to_arbiters(self): minPoolSize=1, # minPoolSize event_listeners=[listener], ) - self.addCleanup(c.close) wait_until(lambda: len(c.nodes) == 3, "connect") - self.assertEqual(c.address, ("a", 1)) - self.assertEqual(c.arbiters, {("c", 3)}) - # Assert that we create 2 and only 2 pooled connections. + assert c.address == ("a", 1) + assert c.arbiters == {("c", 3)} listener.wait_for_event(monitoring.ConnectionReadyEvent, 2) - self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 2) - # Assert that we do not create connections to arbiters. + assert listener.event_count(monitoring.ConnectionCreatedEvent) == 2 arbiter = c._topology.get_server_by_address(("c", 3)) - self.assertFalse(arbiter.pool.conns) - # Assert that we do not create connections to unknown servers. + assert not arbiter.pool.conns arbiter = c._topology.get_server_by_address(("d", 4)) - self.assertFalse(arbiter.pool.conns) - # Arbiter pool is not marked ready. - self.assertEqual(listener.event_count(monitoring.PoolReadyEvent), 2) + assert not arbiter.pool.conns + assert listener.event_count(monitoring.PoolReadyEvent) == 2 - @client_context.require_connection - def test_direct_client_maintains_pool_to_arbiter(self): + def test_direct_client_maintains_pool_to_arbiter(self, mock_client): listener = CMAPListener() - c = MockClient.get_mock_client( + c = mock_client( standalones=[], members=["a:1", "b:2", "c:3"], mongoses=[], @@ -2532,18 +2539,11 @@ def test_direct_client_maintains_pool_to_arbiter(self): minPoolSize=1, # minPoolSize event_listeners=[listener], ) - self.addCleanup(c.close) wait_until(lambda: len(c.nodes) == 1, "connect") - self.assertEqual(c.address, ("c", 3)) - # Assert that we create 1 pooled connection. + assert c.address == ("c", 3) listener.wait_for_event(monitoring.ConnectionReadyEvent, 1) - self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 1) + assert listener.event_count(monitoring.ConnectionCreatedEvent) == 1 arbiter = c._topology.get_server_by_address(("c", 3)) - self.assertEqual(len(arbiter.pool.conns), 1) - # Arbiter pool is marked ready. - self.assertEqual(listener.event_count(monitoring.PoolReadyEvent), 1) - - -if __name__ == "__main__": - unittest.main() + assert len(arbiter.pool.conns) == 1 + assert listener.event_count(monitoring.PoolReadyEvent) == 1 diff --git a/test/test_custom_types.py b/test/test_custom_types.py index 6771ea25f9..65f7994fb1 100644 --- a/test/test_custom_types.py +++ b/test/test_custom_types.py @@ -25,8 +25,7 @@ sys.path[0:0] = [""] -from test import client_context, unittest -from test.test_client import IntegrationTest +from test import IntegrationTest, client_context, unittest from bson import ( _BUILT_IN_TYPES, diff --git a/test/utils.py b/test/utils.py index 69154bc63b..bd8de4ecee 100644 --- a/test/utils.py +++ b/test/utils.py @@ -32,9 +32,10 @@ from collections import abc, defaultdict from functools import partial from test import client_context, db_pwd, db_user -from test.asynchronous import async_client_context from typing import Any, List +import pytest + from bson import json_util from bson.objectid import ObjectId from bson.son import SON @@ -810,7 +811,7 @@ def frequent_thread_switches(): sys.setswitchinterval(interval) -def lazy_client_trial(reset, target, test, get_client): +def lazy_client_trial(reset, target, test, client, client_context): """Test concurrent operations on a lazily-connecting client. `reset` takes a collection and resets it for the next trial. @@ -826,7 +827,7 @@ def lazy_client_trial(reset, target, test, get_client): with frequent_thread_switches(): for _i in range(NTRIALS): reset(collection) - lazy_client = get_client() + lazy_client = client lazy_collection = lazy_client.pymongo_test.test run_threads(lazy_collection, target) test(lazy_collection) @@ -1022,3 +1023,10 @@ async def async_set_fail_point(client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) cmd.update(command_args) await client.admin.command(cmd) + + +def _default_pytest_mark(is_sync: bool): + if is_sync: + return pytest.mark.default + else: + return pytest.mark.asyncio(loop_scope="session") diff --git a/tools/synchro.py b/tools/synchro.py index dbcbbd1351..71b13c94f3 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -75,6 +75,8 @@ "AsyncPyMongoTestCase": "PyMongoTestCase", "AsyncMockClientTest": "MockClientTest", "async_client_context": "client_context", + "async_client": "client", + "async_mock_client": "mock_client", "async_setup": "setup", "asyncSetUp": "setUp", "asyncTearDown": "tearDown", @@ -121,6 +123,10 @@ "_async_cond_wait": "_cond_wait", } +removals: set[str] = { + 'loop_scope="session"', +} + docstring_replacements: dict[tuple[str, str], str] = { ("MongoClient", "connect"): """If ``True`` (the default), immediately begin connecting to MongoDB in the background. Otherwise connect @@ -187,6 +193,7 @@ def async_only_test(f: str) -> bool: "test_bulk.py", "test_change_stream.py", "test_client.py", + "test_client_pytest.py", "test_client_bulk_write.py", "test_client_context.py", "test_collation.py", @@ -229,7 +236,9 @@ def process_files( if file in docstring_translate_files: lines = translate_docstrings(lines) if file in sync_test_files: - translate_imports(lines) + lines = translate_imports(lines) + if file in sync_test_files: + lines = apply_removals(lines) f.seek(0) f.writelines(lines) f.truncate() @@ -331,6 +340,18 @@ def translate_docstrings(lines: list[str]) -> list[str]: return [line for line in lines if line != "DOCSTRING_REMOVED"] +def apply_removals(lines: list[str]) -> list[str]: + tokens_to_remove = [line for line in lines if any(t in line for t in removals)] + for token in removals: + for line in tokens_to_remove: + index = lines.index(line) + if token + ", " in line: + lines[index] = line.replace(token + ", ", "") + else: + lines[index] = line.replace(token, "") + return lines + + def unasync_directory(files: list[str], src: str, dest: str, replacements: dict[str, str]) -> None: unasync_files( files, diff --git a/uv.lock b/uv.lock index e7f09f66fc..8d3d9e2dcc 100644 --- a/uv.lock +++ b/uv.lock @@ -181,15 +181,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, -] - [[package]] name = "charset-normalizer" version = "3.4.1" @@ -269,7 +260,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -285,60 +276,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "coverage" -version = "7.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/d3/3ec80acdd57a0d6a1111b978ade388824f37126446fd6750d38bfaca949c/coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8", size = 798314 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/db/08d54dbc12fdfe5857b06105fd1235bdebb7da7c11cd1a0fae936556162a/coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c", size = 210025 }, - { url = "https://files.pythonhosted.org/packages/a8/ff/02c4bcff1025b4a788aa3933e1cd1474d79de43e0d859273b3319ef43cd3/coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b", size = 210499 }, - { url = "https://files.pythonhosted.org/packages/ab/b1/7820a8ef62adeebd37612af9d2369f4467a3bc2641dea1243450def5489e/coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932", size = 238399 }, - { url = "https://files.pythonhosted.org/packages/2c/0e/23a388f3ce16c5ea01a454fef6a9039115abd40b748027d4fef18b3628a7/coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3", size = 236676 }, - { url = "https://files.pythonhosted.org/packages/f8/81/e871b0d58ca5d6cc27d00b2f668ce09c4643ef00512341f3a592a81fb6cd/coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517", size = 237467 }, - { url = "https://files.pythonhosted.org/packages/95/cb/42a6d34d5840635394f1e172aaa0e7cbd9346155e5004a8ee75d8e434c6b/coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a", size = 243539 }, - { url = "https://files.pythonhosted.org/packages/6a/6a/18b3819919fdfd3e2062a75219b363f895f24ae5b80e72ffe5dfb1a7e9c8/coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880", size = 241725 }, - { url = "https://files.pythonhosted.org/packages/b5/3d/a0650978e8b8f78d269358421b7401acaf7cb89e957b2e1be5205ea5940e/coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58", size = 242913 }, - { url = "https://files.pythonhosted.org/packages/8a/fe/95a74158fa0eda56d39783e918edc6fbb3dd3336be390557fc0a2815ecd4/coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4", size = 212381 }, - { url = "https://files.pythonhosted.org/packages/4c/26/b276e0c70cba5059becce2594a268a2731d5b4f2386e9a6afdf37ffa3d44/coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a", size = 213225 }, - { url = "https://files.pythonhosted.org/packages/71/cf/964bb667ea37d64b25f04d4cfaf6232cdb7a6472e1f4a4faf0459ddcec40/coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375", size = 210130 }, - { url = "https://files.pythonhosted.org/packages/aa/56/31edd4baa132fe2b991437e0acf3e36c50418370044a89b65518e5581f4c/coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb", size = 210617 }, - { url = "https://files.pythonhosted.org/packages/26/6d/4cd14bd0221180c307fae4f8ef00dbd86a13507c25081858c620aa6fafd8/coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95", size = 242048 }, - { url = "https://files.pythonhosted.org/packages/84/60/7eb84255bd9947b140e0382721b0a1b25fd670b4f0f176f11f90b5632d02/coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d", size = 239619 }, - { url = "https://files.pythonhosted.org/packages/76/6b/e8f4696194fdf3c19422f2a80ac10e03a9322f93e6c9ef57a89e03a8c8f7/coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743", size = 241321 }, - { url = "https://files.pythonhosted.org/packages/3f/1c/6a6990fd2e6890807775852882b1ed0a8e50519a525252490b0c219aa8a5/coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1", size = 250419 }, - { url = "https://files.pythonhosted.org/packages/1a/be/b6422a1422381704dd015cc23e503acd1a44a6bdc4e59c75f8c6a2b24151/coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de", size = 248794 }, - { url = "https://files.pythonhosted.org/packages/9b/93/e8231000754d4a31fe9a6c550f6a436eacd2e50763ba2b418f10b2308e45/coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff", size = 249873 }, - { url = "https://files.pythonhosted.org/packages/d3/6f/eb5aae80bf9d01d0f293121d4caa660ac968da2cb967f82547a7b5e8d65b/coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d", size = 212380 }, - { url = "https://files.pythonhosted.org/packages/30/73/b70ab57f11b62f5ca9a83f43cae752fbbb4417bea651875235c32eb2fc2e/coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656", size = 213316 }, - { url = "https://files.pythonhosted.org/packages/36/db/f4e17ffb5ac2d125c72ee3b235c2e04f85a4296a6a9e17730e218af113d8/coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9", size = 210340 }, - { url = "https://files.pythonhosted.org/packages/c3/bc/d7e832280f269be9e8d46cff5c4031b4840f1844674dc53ad93c5a9c1da6/coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64", size = 210612 }, - { url = "https://files.pythonhosted.org/packages/54/84/543e2cd6c1de30c7522a0afcb040677957bac756dd8677bade8bdd9274ba/coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af", size = 242926 }, - { url = "https://files.pythonhosted.org/packages/ad/06/570533f747141b4fd727a193317e16c6e677ed7945e23a195b8f64e685a2/coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc", size = 240294 }, - { url = "https://files.pythonhosted.org/packages/fa/d9/ec4ba0913195d240d026670d41b91f3e5b9a8a143a385f93a09e97c90f5c/coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2", size = 242232 }, - { url = "https://files.pythonhosted.org/packages/d9/3f/1a613c32aa1980d20d6ca2f54faf800df04aafad6016d7132b3276d8715d/coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1", size = 249171 }, - { url = "https://files.pythonhosted.org/packages/b9/3b/e16b12693572fd69148453abc6ddcd20cbeae6f0a040b5ed6af2f75b646f/coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb", size = 247073 }, - { url = "https://files.pythonhosted.org/packages/e7/3e/04a05d40bb09f90a312296a32fb2c5ade2dfcf803edf777ad18b97547503/coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2", size = 248812 }, - { url = "https://files.pythonhosted.org/packages/ba/f7/3a8b7b0affe548227f3d45e248c0f22c5b55bff0ee062b49afc165b3ff25/coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4", size = 212634 }, - { url = "https://files.pythonhosted.org/packages/7c/31/5f5286d2a5e21e1fe5670629bb24c79bf46383a092e74e00077e7a178e5c/coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475", size = 213460 }, - { url = "https://files.pythonhosted.org/packages/62/18/5573216d5b8db7d9f29189350dcd81830a03a624966c35f8201ae10df09c/coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1", size = 210014 }, - { url = "https://files.pythonhosted.org/packages/7c/0e/e98d6c6d569d65ff3195f095e6b006b3d7780fd6182322a25e7dfe0d53d3/coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5", size = 210494 }, - { url = "https://files.pythonhosted.org/packages/d3/63/98e5a6b7ed1bfca874729ee309cc49a6d6658ab9e479a2b6d223ccc96e03/coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631", size = 237996 }, - { url = "https://files.pythonhosted.org/packages/76/e4/d3c67a0a092127b8a3dffa2f75334a8cdb2cefc99e3d75a7f42cf1ff98a9/coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46", size = 236287 }, - { url = "https://files.pythonhosted.org/packages/12/7f/9b787ffc31bc39aa9e98c7005b698e7c6539bd222043e4a9c83b83c782a2/coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e", size = 237070 }, - { url = "https://files.pythonhosted.org/packages/31/ee/9998a0d855cad5f8e04062f7428b83c34aa643e5df468409593a480d5585/coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be", size = 243115 }, - { url = "https://files.pythonhosted.org/packages/16/94/1e348cd4445404c588ec8199adde0b45727b1d7989d8fb097d39c93e3da5/coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b", size = 241315 }, - { url = "https://files.pythonhosted.org/packages/28/17/6fe1695d2a706e586b87a407598f4ed82dd218b2b43cdc790f695f259849/coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0", size = 242467 }, - { url = "https://files.pythonhosted.org/packages/81/a2/1e550272c8b1f89b980504230b1a929de83d8f3d5ecb268477b32e5996a6/coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7", size = 212394 }, - { url = "https://files.pythonhosted.org/packages/c9/48/7d3c31064c5adcc743fe5370cf7e198cee06cc0e2d37b5cbe930691a3f54/coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493", size = 213246 }, - { url = "https://files.pythonhosted.org/packages/34/81/f00ce7ef95479085feb01fa9e352b2b5b2b9d24767acf2266d6267a6dba9/coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067", size = 202381 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cramjam" version = "2.9.1" @@ -468,15 +405,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, ] -[[package]] -name = "distlib" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, -] - [[package]] name = "dnspython" version = "2.7.0" @@ -495,19 +423,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] -[[package]] -name = "eventlet" -version = "0.38.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "greenlet" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/4e/f974cc85b8d19b31176e0cca90e1650156f385c9c294a96fc42846ca75e9/eventlet-0.38.2.tar.gz", hash = "sha256:6a46823af1dca7d29cf04c0d680365805435473c3acbffc176765c7f8787edac", size = 561526 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/07/00feb2c708d71796e190a3051a0d530a4922bfb6b346aa8302725840698c/eventlet-0.38.2-py3-none-any.whl", hash = "sha256:4a2e3cbc53917c8f39074ccf689501168563d3a4df59e9cddd5e9d3b7f85c599", size = 363192 }, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -517,15 +432,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] -[[package]] -name = "filelock" -version = "3.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, -] - [[package]] name = "furo" version = "2024.8.6" @@ -542,118 +448,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, ] -[[package]] -name = "gevent" -version = "24.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, - { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, - { name = "zope-event" }, - { name = "zope-interface" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/75/a53f1cb732420f5e5d79b2563fc3504d22115e7ecfe7966e5cf9b3582ae7/gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca", size = 5976624 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/7d/27ed3603f4bf96b36fb2746e923e033bc600c6684de8fe164d64eb8c4dcc/gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e", size = 2998254 }, - { url = "https://files.pythonhosted.org/packages/a8/03/a8f6c70f50a644a79e75d9f15e6f1813115d34c3c55528e4669a9316534d/gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870", size = 4817711 }, - { url = "https://files.pythonhosted.org/packages/f0/05/4f9bc565520a18f107464d40ac15a91708431362c797e77fbb5e7ff26e64/gevent-24.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7398c629d43b1b6fd785db8ebd46c0a353880a6fab03d1cf9b6788e7240ee32e", size = 4934468 }, - { url = "https://files.pythonhosted.org/packages/4a/7d/f15561eeebecbebc0296dd7bebea10ac4af0065d98249e3d8c4998e68edd/gevent-24.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7886b63ebfb865178ab28784accd32f287d5349b3ed71094c86e4d3ca738af5", size = 5014067 }, - { url = "https://files.pythonhosted.org/packages/67/c1/07eff117a600fc3c9bd4e3a1ff3b726f146ee23ce55981156547ccae0c85/gevent-24.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ca80711e6553880974898d99357fb649e062f9058418a92120ca06c18c3c59", size = 6625531 }, - { url = "https://files.pythonhosted.org/packages/4b/72/43f76ab6b18e5e56b1003c844829971f3044af08b39b3c9040559be00a2b/gevent-24.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e24181d172f50097ac8fc272c8c5b030149b630df02d1c639ee9f878a470ba2b", size = 5249671 }, - { url = "https://files.pythonhosted.org/packages/6b/fc/1a847ada0757cc7690f83959227514b1a52ff6de504619501c81805fa1da/gevent-24.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1d4fadc319b13ef0a3c44d2792f7918cf1bca27cacd4d41431c22e6b46668026", size = 6773903 }, - { url = "https://files.pythonhosted.org/packages/3b/9d/254dcf455f6659ab7e36bec0bc11f51b18ea25eac2de69185e858ccf3c30/gevent-24.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d882faa24f347f761f934786dde6c73aa6c9187ee710189f12dcc3a63ed4a50", size = 1560443 }, - { url = "https://files.pythonhosted.org/packages/ea/fd/86a170f77ef51a15297573c50dbec4cc67ddc98b677cc2d03cc7f2927f4c/gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62", size = 2951424 }, - { url = "https://files.pythonhosted.org/packages/7f/0a/987268c9d446f61883bc627c77c5ed4a97869c0f541f76661a62b2c411f6/gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab", size = 4878504 }, - { url = "https://files.pythonhosted.org/packages/dc/d4/2f77ddd837c0e21b4a4460bcb79318b6754d95ef138b7a29f3221c7e9993/gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758", size = 5007668 }, - { url = "https://files.pythonhosted.org/packages/80/a0/829e0399a1f9b84c344b72d2be9aa60fe2a64e993cac221edcc14f069679/gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d", size = 5067055 }, - { url = "https://files.pythonhosted.org/packages/1e/67/0e693f9ddb7909c2414f8fcfc2409aa4157884c147bc83dab979e9cf717c/gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546", size = 6761883 }, - { url = "https://files.pythonhosted.org/packages/fa/b6/b69883fc069d7148dd23c5dda20826044e54e7197f3c8e72b8cc2cd4035a/gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c", size = 5440802 }, - { url = "https://files.pythonhosted.org/packages/32/4e/b00094d995ff01fd88b3cf6b9d1d794f935c31c645c431e65cd82d808c9c/gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61", size = 6866992 }, - { url = "https://files.pythonhosted.org/packages/37/ed/58dbe9fb09d36f6477ff8db0459ebd3be9a77dc05ae5d96dc91ad657610d/gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897", size = 1543736 }, - { url = "https://files.pythonhosted.org/packages/dd/32/301676f67ffa996ff1c4175092fb0c48c83271cc95e5c67650b87156b6cf/gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46", size = 2956467 }, - { url = "https://files.pythonhosted.org/packages/6b/84/aef1a598123cef2375b6e2bf9d17606b961040f8a10e3dcc3c3dd2a99f05/gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb", size = 5136486 }, - { url = "https://files.pythonhosted.org/packages/92/7b/04f61187ee1df7a913b3fca63b0a1206c29141ab4d2a57e7645237b6feb5/gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85", size = 5299718 }, - { url = "https://files.pythonhosted.org/packages/36/2a/ebd12183ac25eece91d084be2111e582b061f4d15ead32239b43ed47e9ba/gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6", size = 5400118 }, - { url = "https://files.pythonhosted.org/packages/ec/c9/f006c0cd59f0720fbb62ee11da0ad4c4c0fd12799afd957dd491137e80d9/gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671", size = 6775163 }, - { url = "https://files.pythonhosted.org/packages/49/f1/5edf00b674b10d67e3b967c2d46b8a124c2bc8cfd59d4722704392206444/gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f", size = 5479886 }, - { url = "https://files.pythonhosted.org/packages/22/11/c48e62744a32c0d48984268ae62b99edb81eaf0e03b42de52e2f09855509/gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a", size = 6891452 }, - { url = "https://files.pythonhosted.org/packages/11/b2/5d20664ef6a077bec9f27f7a7ee761edc64946d0b1e293726a3d074a9a18/gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae", size = 1541631 }, - { url = "https://files.pythonhosted.org/packages/a4/8f/4958e70caeaf469c576ecc5b5f2cb49ddaad74336fa82363d89cddb3c284/gevent-24.11.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d618e118fdb7af1d6c1a96597a5cd6ac84a9f3732b5be8515c6a66e098d498b6", size = 2949601 }, - { url = "https://files.pythonhosted.org/packages/3b/64/79892d250b7b2aa810688dfebe783aec02568e5cecacb1e100acbb9d95c6/gevent-24.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2142704c2adce9cd92f6600f371afb2860a446bfd0be5bd86cca5b3e12130766", size = 5107052 }, - { url = "https://files.pythonhosted.org/packages/66/44/9ee0ed1909b4f41375e32bf10036d5d8624962afcbd901573afdecd2e36a/gevent-24.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92e0d7759de2450a501effd99374256b26359e801b2d8bf3eedd3751973e87f5", size = 5271736 }, - { url = "https://files.pythonhosted.org/packages/e3/48/0184b2622a388a256199c5fadcad6b52b6455019c2a4b19edd6de58e30ba/gevent-24.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca845138965c8c56d1550499d6b923eb1a2331acfa9e13b817ad8305dde83d11", size = 5367782 }, - { url = "https://files.pythonhosted.org/packages/9a/b1/1a2704c346234d889d2e0042efb182534f7d294115f0e9f99d8079fa17eb/gevent-24.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:356b73d52a227d3313f8f828025b665deada57a43d02b1cf54e5d39028dbcf8d", size = 6757533 }, - { url = "https://files.pythonhosted.org/packages/ed/6e/b2eed8dec617264f0046d50a13a42d3f0a06c50071b9fc1eae00285a03f1/gevent-24.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:58851f23c4bdb70390f10fc020c973ffcf409eb1664086792c8b1e20f25eef43", size = 5449436 }, - { url = "https://files.pythonhosted.org/packages/63/c2/eca6b95fbf9af287fa91c327494e4b74a8d5bfa0156cd87b233f63f118dc/gevent-24.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1ea50009ecb7f1327347c37e9eb6561bdbc7de290769ee1404107b9a9cba7cf1", size = 6866470 }, - { url = "https://files.pythonhosted.org/packages/b7/e6/51824bd1f2c1ce70aa01495aa6ffe04ab789fa819fa7e6f0ad2388fb03c6/gevent-24.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:ec68e270543ecd532c4c1d70fca020f90aa5486ad49c4f3b8b2e64a66f5c9274", size = 1540088 }, - { url = "https://files.pythonhosted.org/packages/a0/73/263d0f63186d27d205b3dc157efe838afe3aba10a3baca15d85e97b90eae/gevent-24.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9347690f4e53de2c4af74e62d6fabc940b6d4a6cad555b5a379f61e7d3f2a8e", size = 6658480 }, - { url = "https://files.pythonhosted.org/packages/8a/fd/ec7b5c764a3d1340160b82f7394fdc1220d18e11ae089c472cf7bcc2fe6a/gevent-24.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8619d5c888cb7aebf9aec6703e410620ef5ad48cdc2d813dd606f8aa7ace675f", size = 6808247 }, - { url = "https://files.pythonhosted.org/packages/95/82/2ce68dc8dbc2c3ed3f4e73f21e1b7a45d80b5225670225a48e695f248850/gevent-24.11.1-cp39-cp39-win32.whl", hash = "sha256:c6b775381f805ff5faf250e3a07c0819529571d19bb2a9d474bee8c3f90d66af", size = 1483133 }, - { url = "https://files.pythonhosted.org/packages/76/96/aa4cbcf1807187b65a9c9ff15b32b08c2014968be852dda34d212cf8cc58/gevent-24.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c3443b0ed23dcb7c36a748d42587168672953d368f2956b17fad36d43b58836", size = 1566354 }, - { url = "https://files.pythonhosted.org/packages/86/63/197aa67250943b508b34995c2aa6b46402e7e6f11785487740c2057bfb20/gevent-24.11.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:f43f47e702d0c8e1b8b997c00f1601486f9f976f84ab704f8f11536e3fa144c9", size = 1271676 }, -] - -[[package]] -name = "greenlet" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, - { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, - { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, - { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, - { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, - { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, - { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, - { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, - { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, - { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, - { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, - { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, - { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, - { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, - { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, - { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, - { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, - { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, - { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, - { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, - { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, - { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, - { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, - { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, - { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, - { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, - { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, - { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, - { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, - { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, - { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, - { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, - { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, - { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, - { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, - { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, -] - [[package]] name = "h11" version = "0.14.0" @@ -691,15 +485,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] -[[package]] -name = "identify" -version = "2.6.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 }, -] - [[package]] name = "idna" version = "3.10" @@ -828,76 +613,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, ] -[[package]] -name = "mockupdb" -version = "1.9.0.dev1" -source = { git = "https://github.com/mongodb-labs/mongo-mockup-db?rev=master#317c4e049965f9d99423698a81e52d0ab37b7599" } -dependencies = [ - { name = "pymongo" }, -] - -[[package]] -name = "mypy" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, - { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493 }, - { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702 }, - { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104 }, - { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167 }, - { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834 }, - { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231 }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, -] - [[package]] name = "packaging" version = "24.2" @@ -907,24 +622,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] -[[package]] -name = "pip" -version = "24.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - [[package]] name = "pluggy" version = "1.5.0" @@ -934,22 +631,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] -[[package]] -name = "pre-commit" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, -] - [[package]] name = "pyasn1" version = "0.6.1" @@ -1017,7 +698,7 @@ docs = [ { name = "sphinxcontrib-shellcheck" }, ] encryption = [ - { name = "certifi", marker = "os_name == 'nt' or sys_platform == 'darwin'" }, + { name = "certifi", marker = "sys_platform == 'darwin' or os_name == 'nt'" }, { name = "pymongo-auth-aws" }, { name = "pymongocrypt" }, ] @@ -1026,7 +707,7 @@ gssapi = [ { name = "winkerberos", marker = "os_name == 'nt'" }, ] ocsp = [ - { name = "certifi", marker = "os_name == 'nt' or sys_platform == 'darwin'" }, + { name = "certifi", marker = "sys_platform == 'darwin' or os_name == 'nt'" }, { name = "cryptography" }, { name = "pyopenssl" }, { name = "requests" }, @@ -1043,40 +724,10 @@ zstd = [ { name = "zstandard" }, ] -[package.dev-dependencies] -coverage = [ - { name = "coverage" }, - { name = "pytest-cov" }, -] -dev = [ - { name = "pre-commit" }, -] -eventlet = [ - { name = "eventlet" }, -] -gevent = [ - { name = "gevent" }, -] -mockupdb = [ - { name = "mockupdb" }, -] -perf = [ - { name = "simplejson" }, -] -pymongocrypt-source = [ - { name = "pymongocrypt" }, -] -typing = [ - { name = "mypy" }, - { name = "pip" }, - { name = "pyright" }, - { name = "typing-extensions" }, -] - [package.metadata] requires-dist = [ - { name = "certifi", marker = "(os_name == 'nt' and extra == 'encryption') or (sys_platform == 'darwin' and extra == 'encryption')" }, - { name = "certifi", marker = "(os_name == 'nt' and extra == 'ocsp') or (sys_platform == 'darwin' and extra == 'ocsp')" }, + { name = "certifi", marker = "(sys_platform == 'darwin' and extra == 'encryption') or (os_name == 'nt' and extra == 'encryption')" }, + { name = "certifi", marker = "(sys_platform == 'darwin' and extra == 'ocsp') or (os_name == 'nt' and extra == 'ocsp')" }, { name = "cryptography", marker = "extra == 'ocsp'", specifier = ">=2.5" }, { name = "dnspython", specifier = ">=1.16.0,<3.0.0" }, { name = "furo", marker = "extra == 'docs'", specifier = "==2024.8.6" }, @@ -1099,24 +750,6 @@ requires-dist = [ { name = "zstandard", marker = "extra == 'zstd'" }, ] -[package.metadata.requires-dev] -coverage = [ - { name = "coverage", specifier = ">=5,<=7.5" }, - { name = "pytest-cov" }, -] -dev = [{ name = "pre-commit", specifier = ">=4.0" }] -eventlet = [{ name = "eventlet" }] -gevent = [{ name = "gevent" }] -mockupdb = [{ name = "mockupdb", git = "https://github.com/mongodb-labs/mongo-mockup-db?rev=master" }] -perf = [{ name = "simplejson" }] -pymongocrypt-source = [{ name = "pymongocrypt", git = "https://github.com/mongodb/libmongocrypt?subdirectory=bindings%2Fpython&rev=master" }] -typing = [ - { name = "mypy", specifier = "==1.14.1" }, - { name = "pip" }, - { name = "pyright", specifier = "==1.1.392.post0" }, - { name = "typing-extensions" }, -] - [[package]] name = "pymongo-auth-aws" version = "1.3.0" @@ -1132,14 +765,21 @@ wheels = [ [[package]] name = "pymongocrypt" -version = "1.13.0.dev0" -source = { git = "https://github.com/mongodb/libmongocrypt?subdirectory=bindings%2Fpython&rev=master#90476d5db7737bab2ce1c198df5671a12dbaae1a" } +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "cryptography" }, { name = "httpx" }, { name = "packaging" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/f8/79/8d30f1b4bd0fcf00f3433d8bb23cec65409fadaf8a0fe69e35b72e0d5aef/pymongocrypt-1.12.2.tar.gz", hash = "sha256:63214edf14274e19a3b0ba6d0e37067c2bb9714b15a891c08c239d2592a3f441", size = 62081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/4e/9f08034e4e7e208747150314d95d9cb572d52a72507f293d9913dac04bd4/pymongocrypt-1.12.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3b30fe6fd71ba9550318f6f7520454c0cef7e0505e1c30e962628c8e0e23a728", size = 4685815 }, + { url = "https://files.pythonhosted.org/packages/f0/07/cc7190d1ef5b23789724079bb91a4862d1a79fb31551d26f463eeb40faf4/pymongocrypt-1.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:755ca20a1f4fbd7e22a79ad4a774647e6cc0a48ebbe6458098ef23fc8e214987", size = 3736188 }, + { url = "https://files.pythonhosted.org/packages/d4/5a/f5f707e0a364261de8056237a65b6589e0d5324ef16084212a6429973f2d/pymongocrypt-1.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8888d950934ca1169fd85bb11df84edf994ccd560568bda86d1cb708414b3e88", size = 3497068 }, + { url = "https://files.pythonhosted.org/packages/c3/17/d2da8cb9b2421aade365abd18f55b91fc220a621dd4387fa6b79d7e6c606/pymongocrypt-1.12.2-py3-none-win_amd64.whl", hash = "sha256:bb0bfb8753e5c43cebe12754e2fc292e64ef6fc1cada4357b58f7afae91cc47a", size = 1556272 }, +] [[package]] name = "pyopenssl" @@ -1154,19 +794,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/d7/eb76863d2060dcbe7c7e6cccfd95ac02ea0b9acc37745a0d99ff6457aefb/pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90", size = 56453 }, ] -[[package]] -name = "pyright" -version = "1.1.392.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/df/3c6f6b08fba7ccf49b114dfc4bb33e25c299883fd763f93fad47ef8bc58d/pyright-1.1.392.post0.tar.gz", hash = "sha256:3b7f88de74a28dcfa90c7d90c782b6569a48c2be5f9d4add38472bdaac247ebd", size = 3789911 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/b1/a18de17f40e4f61ca58856b9ef9b0febf74ff88978c3f7776f910071f567/pyright-1.1.392.post0-py3-none-any.whl", hash = "sha256:252f84458a46fa2f0fd4e2f91fc74f50b9ca52c757062e93f6c250c0d8329eb2", size = 5595487 }, -] - [[package]] name = "pytest" version = "8.3.4" @@ -1196,19 +823,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/d8/defa05ae50dcd6019a95527200d3b3980043df5aa445d40cb0ef9f7f98ab/pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075", size = 19400 }, ] -[[package]] -name = "pytest-cov" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1233,59 +847,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/c1/0ee413ddd639aebf22c85d6db39f136ccc10e6a4b4dd275a92b5c839de8d/python_snappy-0.7.3-py3-none-any.whl", hash = "sha256:074c0636cfcd97e7251330f428064050ac81a52c62ed884fc2ddebbb60ed7f50", size = 9155 }, ] -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, -] - [[package]] name = "readthedocs-sphinx-search" version = "0.3.2" @@ -1338,89 +899,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 }, ] -[[package]] -name = "setuptools" -version = "75.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, -] - -[[package]] -name = "simplejson" -version = "3.19.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/29/085111f19717f865eceaf0d4397bf3e76b08d60428b076b64e2a1903706d/simplejson-3.19.3.tar.gz", hash = "sha256:8e086896c36210ab6050f2f9f095a5f1e03c83fa0e7f296d6cba425411364680", size = 85237 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/24/260ad03435ce8ef2436031951134659c7161776ec3a78094b35b9375ceea/simplejson-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50d8b742d74c449c4dcac570d08ce0f21f6a149d2d9cf7652dbf2ba9a1bc729a", size = 93660 }, - { url = "https://files.pythonhosted.org/packages/63/a1/dee207f357bcd6b106f2ca5129ee916c24993ba08b7dfbf9a37c22442ea9/simplejson-3.19.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd011fc3c1d88b779645495fdb8189fb318a26981eebcce14109460e062f209b", size = 75546 }, - { url = "https://files.pythonhosted.org/packages/80/7b/45ef1da43f54d209ce2ef59b7356cda13f810186c381f38ae23a4d2b1337/simplejson-3.19.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:637c4d4b81825c1f4d651e56210bd35b5604034b192b02d2d8f17f7ce8c18f42", size = 75602 }, - { url = "https://files.pythonhosted.org/packages/7f/4b/9a132382982f8127bc7ce5212a5585d83c174707c9dd698d0cb6a0d41882/simplejson-3.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f56eb03bc9e432bb81adc8ecff2486d39feb371abb442964ffb44f6db23b332", size = 138632 }, - { url = "https://files.pythonhosted.org/packages/76/37/012f5ad2f38afa28f8a6ad9da01dc0b64492ffbaf2a3f2f8a0e1fddf9c1d/simplejson-3.19.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef59a53be400c1fad2c914b8d74c9d42384fed5174f9321dd021b7017fd40270", size = 146740 }, - { url = "https://files.pythonhosted.org/packages/69/b3/89640bd676e26ea2315b5aaf80712a6fbbb4338e4caf872d91448502a19b/simplejson-3.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72e8abbc86fcac83629a030888b45fed3a404d54161118be52cb491cd6975d3e", size = 134440 }, - { url = "https://files.pythonhosted.org/packages/61/20/0035a288deaff05397d6cc0145b33f3dd2429b99cdc880de4c5eca41ca72/simplejson-3.19.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8efb03ca77bd7725dfacc9254df00d73e6f43013cf39bd37ef1a8ed0ebb5165", size = 137949 }, - { url = "https://files.pythonhosted.org/packages/5d/de/5b03fafe3003e32d179588953d38183af6c3747e95c7dcc668c4f9eb886a/simplejson-3.19.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:add8850db04b98507a8b62d248a326ecc8561e6d24336d1ca5c605bbfaab4cad", size = 139992 }, - { url = "https://files.pythonhosted.org/packages/d1/ce/e493116ff49fd215f7baa25195b8f684c91e65c153e2a57e04dc3f3a466b/simplejson-3.19.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fc3dc9fb413fc34c396f52f4c87de18d0bd5023804afa8ab5cc224deeb6a9900", size = 140320 }, - { url = "https://files.pythonhosted.org/packages/86/f3/a18b98a7a27548829f672754dd3940fb637a27981399838128d3e560087f/simplejson-3.19.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dfa420bb9225dd33b6efdabde7c6a671b51150b9b1d9c4e5cd74d3b420b3fe1", size = 148625 }, - { url = "https://files.pythonhosted.org/packages/0f/55/d3da33ee3e708133da079b9d537693d7fef281e6f0d27921cc7e5b3ec523/simplejson-3.19.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7b5c472099b39b274dcde27f1113db8d818c9aa3ba8f78cbb8ad04a4c1ac2118", size = 141287 }, - { url = "https://files.pythonhosted.org/packages/17/e8/56184ab4d66bb64a6ff569f069b3796dfd943f9b961268fe0d403526fc17/simplejson-3.19.3-cp310-cp310-win32.whl", hash = "sha256:817abad79241ed4a507b3caf4d3f2be5079f39d35d4c550a061988986bffd2ec", size = 74143 }, - { url = "https://files.pythonhosted.org/packages/be/8f/a0089eff060f10a925f08b0a0f50854321484f1ac54b1895bbf4c9213dfe/simplejson-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:dd5b9b1783e14803e362a558680d88939e830db2466f3fa22df5c9319f8eea94", size = 75643 }, - { url = "https://files.pythonhosted.org/packages/8c/bb/9ee3959e6929d228cf669b3f13f0edd43c5261b6cd69598640748b19ca35/simplejson-3.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e88abff510dcff903a18d11c2a75f9964e768d99c8d147839913886144b2065e", size = 91930 }, - { url = "https://files.pythonhosted.org/packages/ac/ae/a06523928af3a6783e2638cd4f6035c3e32de1c1063d563d9060c8d2f1ad/simplejson-3.19.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:934a50a614fb831614db5dbfba35127ee277624dda4d15895c957d2f5d48610c", size = 74787 }, - { url = "https://files.pythonhosted.org/packages/c3/58/fea732e48a7540035fe46d39e6fd77679f5810311d31da8661ce7a18210a/simplejson-3.19.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:212fce86a22188b0c7f53533b0f693ea9605c1a0f02c84c475a30616f55a744d", size = 74612 }, - { url = "https://files.pythonhosted.org/packages/ab/4d/15718f20cb0e3875b8af9597d6bb3bfbcf1383834b82b6385ee9ac0b72a9/simplejson-3.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d9e8f836688a8fabe6a6b41b334aa550a6823f7b4ac3d3712fc0ad8655be9a8", size = 143550 }, - { url = "https://files.pythonhosted.org/packages/93/44/815a4343774760f7a82459c8f6a4d8268b4b6d23f81e7b922a5e2ca79171/simplejson-3.19.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23228037dc5d41c36666384062904d74409a62f52283d9858fa12f4c22cffad1", size = 153284 }, - { url = "https://files.pythonhosted.org/packages/9d/52/d3202d9bba95444090d1c98e43da3c10907875babf63ed3c134d1b9437e3/simplejson-3.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0791f64fed7d4abad639491f8a6b1ba56d3c604eb94b50f8697359b92d983f36", size = 141518 }, - { url = "https://files.pythonhosted.org/packages/b7/d4/850948bcbcfe0b4a6c69dfde10e245d3a1ea45252f16a1e2308a3b06b1da/simplejson-3.19.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f614581b61a26fbbba232a1391f6cee82bc26f2abbb6a0b44a9bba25c56a1c", size = 144688 }, - { url = "https://files.pythonhosted.org/packages/58/d2/b8dcb0a07d9cd54c47f9fe8733dbb83891d1efe4fc786d9dfc8781cc04f9/simplejson-3.19.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1df0aaf1cb787fdf34484ed4a1f0c545efd8811f6028623290fef1a53694e597", size = 144534 }, - { url = "https://files.pythonhosted.org/packages/a9/95/1e92d99039041f596e0923ec4f9153244acaf3830944dc69a7c11b23ceaa/simplejson-3.19.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:951095be8d4451a7182403354c22ec2de3e513e0cc40408b689af08d02611588", size = 146565 }, - { url = "https://files.pythonhosted.org/packages/21/04/c96aeb3a74031255e4cbcc0ca1b6ebfb5549902f0a065f06d65ce8447c0c/simplejson-3.19.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a954b30810988feeabde843e3263bf187697e0eb5037396276db3612434049b", size = 155014 }, - { url = "https://files.pythonhosted.org/packages/b7/41/e28a28593afc4a75d8999d057bfb7c73a103e35f927e66f4bb92571787ae/simplejson-3.19.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c40df31a75de98db2cdfead6074d4449cd009e79f54c1ebe5e5f1f153c68ad20", size = 148092 }, - { url = "https://files.pythonhosted.org/packages/2b/82/1c81a3af06f937afb6d2e9d74a465c0e0ae6db444d1bf2a436ea26de1965/simplejson-3.19.3-cp311-cp311-win32.whl", hash = "sha256:7e2a098c21ad8924076a12b6c178965d88a0ad75d1de67e1afa0a66878f277a5", size = 73942 }, - { url = "https://files.pythonhosted.org/packages/65/be/d8ab9717f471be3c114f16abd8be21d9a6a0a09b9b49177d93d64d3717d9/simplejson-3.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:c9bedebdc5fdad48af8783022bae307746d54006b783007d1d3c38e10872a2c6", size = 75469 }, - { url = "https://files.pythonhosted.org/packages/20/15/513fea93fafbdd4993eacfcb762965b2ff3d29e618c029e2956174d68c4b/simplejson-3.19.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:66a0399e21c2112acacfebf3d832ebe2884f823b1c7e6d1363f2944f1db31a99", size = 92921 }, - { url = "https://files.pythonhosted.org/packages/a4/4f/998a907ae1a6c104dc0ee48aa248c2478490152808d34d8e07af57f396c3/simplejson-3.19.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6ef9383c5e05f445be60f1735c1816163c874c0b1ede8bb4390aff2ced34f333", size = 75311 }, - { url = "https://files.pythonhosted.org/packages/db/44/acd6122201e927451869d45952b9ab1d3025cdb5e61548d286d08fbccc08/simplejson-3.19.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42e5acf80d4d971238d4df97811286a044d720693092b20a56d5e56b7dcc5d09", size = 74964 }, - { url = "https://files.pythonhosted.org/packages/27/ca/d0a1e8f16e1bbdc0b8c6d88166f45f565ed7285f53928cfef3b6ce78f14d/simplejson-3.19.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0b0efc7279d768db7c74d3d07f0b5c81280d16ae3fb14e9081dc903e8360771", size = 150106 }, - { url = "https://files.pythonhosted.org/packages/63/59/0554b78cf26c98e2b9cae3f44723bd72c2394e2afec1a14eedc6211f7187/simplejson-3.19.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0552eb06e7234da892e1d02365cd2b7b2b1f8233aa5aabdb2981587b7cc92ea0", size = 158347 }, - { url = "https://files.pythonhosted.org/packages/b2/fe/9f30890352e431e8508cc569912d3322147d3e7e4f321e48c0adfcb4c97d/simplejson-3.19.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf6a3b9a7d7191471b464fe38f684df10eb491ec9ea454003edb45a011ab187", size = 148456 }, - { url = "https://files.pythonhosted.org/packages/37/e3/663a09542ee021d4131162f7a164cb2e7f04ef48433a67591738afbf12ea/simplejson-3.19.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7017329ca8d4dca94ad5e59f496e5fc77630aecfc39df381ffc1d37fb6b25832", size = 152190 }, - { url = "https://files.pythonhosted.org/packages/31/20/4e0c4d35e10ff6465003bec304316d822a559a1c38c66ef6892ca199c207/simplejson-3.19.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:67a20641afebf4cfbcff50061f07daad1eace6e7b31d7622b6fa2c40d43900ba", size = 149846 }, - { url = "https://files.pythonhosted.org/packages/08/7a/46e2e072cac3987cbb05946f25167f0ad2fe536748e7405953fd6661a486/simplejson-3.19.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd6a7dabcc4c32daf601bc45e01b79175dde4b52548becea4f9545b0a4428169", size = 151714 }, - { url = "https://files.pythonhosted.org/packages/7f/7d/dbeeac10eb61d5d8858d0bb51121a21050d281dc83af4c557f86da28746c/simplejson-3.19.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:08f9b443a94e72dd02c87098c96886d35790e79e46b24e67accafbf13b73d43b", size = 158777 }, - { url = "https://files.pythonhosted.org/packages/fc/8f/a98bdbb799c6a4a884b5823db31785a96ba895b4b0f4d8ac345d6fe98bbf/simplejson-3.19.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa97278ae6614346b5ca41a45a911f37a3261b57dbe4a00602048652c862c28b", size = 154230 }, - { url = "https://files.pythonhosted.org/packages/b1/db/852eebceb85f969ae40e06babed1a93d3bacb536f187d7a80ff5823a5979/simplejson-3.19.3-cp312-cp312-win32.whl", hash = "sha256:ef28c3b328d29b5e2756903aed888960bc5df39b4c2eab157ae212f70ed5bf74", size = 74002 }, - { url = "https://files.pythonhosted.org/packages/fe/68/9f0e5df0651cb79ef83cba1378765a00ee8038e6201cc82b8e7178a7778e/simplejson-3.19.3-cp312-cp312-win_amd64.whl", hash = "sha256:1e662336db50ad665777e6548b5076329a94a0c3d4a0472971c588b3ef27de3a", size = 75596 }, - { url = "https://files.pythonhosted.org/packages/93/3a/5896821ed543899fcb9c4256c7e71bb110048047349a00f42bc8b8fb379f/simplejson-3.19.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0959e6cb62e3994b5a40e31047ff97ef5c4138875fae31659bead691bed55896", size = 92931 }, - { url = "https://files.pythonhosted.org/packages/39/15/5d33d269440912ee40d856db0c8be2b91aba7a219690ab01f86cb0edd590/simplejson-3.19.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a7bfad839c624e139a4863007233a3f194e7c51551081f9789cba52e4da5167", size = 75318 }, - { url = "https://files.pythonhosted.org/packages/2a/8d/2e7483a2bf7ec53acf7e012bafbda79d7b34f90471dda8e424544a59d484/simplejson-3.19.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afab2f7f2486a866ff04d6d905e9386ca6a231379181a3838abce1f32fbdcc37", size = 74971 }, - { url = "https://files.pythonhosted.org/packages/4d/9d/9bdf34437c8834a7cf7246f85e9d5122e30579f512c10a0c2560e994294f/simplejson-3.19.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00313681015ac498e1736b304446ee6d1c72c5b287cd196996dad84369998f7", size = 150112 }, - { url = "https://files.pythonhosted.org/packages/a7/e2/1f2ae2d89eaf85f6163c82150180aae5eaa18085cfaf892f8a57d4c51cbd/simplejson-3.19.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d936ae682d5b878af9d9eb4d8bb1fdd5e41275c8eb59ceddb0aeed857bb264a2", size = 158354 }, - { url = "https://files.pythonhosted.org/packages/60/83/26f610adf234c8492b3f30501e12f2271e67790f946c6898fe0c58aefe99/simplejson-3.19.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c6657485393f2e9b8177c77a7634f13ebe70d5e6de150aae1677d91516ce6b", size = 148455 }, - { url = "https://files.pythonhosted.org/packages/b5/4b/109af50006af77133653c55b5b91b4bd2d579ff8254ce11216c0b75f911b/simplejson-3.19.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a6a750d3c7461b1c47cfc6bba8d9e57a455e7c5f80057d2a82f738040dd1129", size = 152191 }, - { url = "https://files.pythonhosted.org/packages/75/dc/108872a8825cbd99ae6f4334e0490ff1580367baf12198bcaf988f6820ba/simplejson-3.19.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea7a4a998c87c5674a27089e022110a1a08a7753f21af3baf09efe9915c23c3c", size = 149954 }, - { url = "https://files.pythonhosted.org/packages/eb/be/deec1d947a5d0472276ab4a4d1a9378dc5ee27f3dc9e54d4f62ffbad7a08/simplejson-3.19.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6300680d83a399be2b8f3b0ef7ef90b35d2a29fe6e9c21438097e0938bbc1564", size = 151812 }, - { url = "https://files.pythonhosted.org/packages/e9/58/4ee130702d36b1551ef66e7587eefe56651f3669255bf748cd71691e2434/simplejson-3.19.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ab69f811a660c362651ae395eba8ce84f84c944cea0df5718ea0ba9d1e4e7252", size = 158880 }, - { url = "https://files.pythonhosted.org/packages/0f/e1/59cc6a371b60f89e3498d9f4c8109f6b7359094d453f5fe80b2677b777b0/simplejson-3.19.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:256e09d0f94d9c3d177d9e95fd27a68c875a4baa2046633df387b86b652f5747", size = 154344 }, - { url = "https://files.pythonhosted.org/packages/79/45/1b36044670016f5cb25ebd92497427d2d1711ecb454d00f71eb9a00b77cc/simplejson-3.19.3-cp313-cp313-win32.whl", hash = "sha256:2c78293470313aefa9cfc5e3f75ca0635721fb016fb1121c1c5b0cb8cc74712a", size = 74002 }, - { url = "https://files.pythonhosted.org/packages/e2/58/b06226e6b0612f2b1fa13d5273551da259f894566b1eef32249ddfdcce44/simplejson-3.19.3-cp313-cp313-win_amd64.whl", hash = "sha256:3bbcdc438dc1683b35f7a8dc100960c721f922f9ede8127f63bed7dfded4c64c", size = 75599 }, - { url = "https://files.pythonhosted.org/packages/9a/3d/e7f1caf7fa8c004c30e2c0595a22646a178344a7f53924c11c3d263a8623/simplejson-3.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b5587feda2b65a79da985ae6d116daf6428bf7489992badc29fc96d16cd27b05", size = 93646 }, - { url = "https://files.pythonhosted.org/packages/01/40/ff5cae1b4ff35c7822456ad7d098371d697479d418194064b8aff8142d70/simplejson-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0d2b00ecbcd1a3c5ea1abc8bb99a26508f758c1759fd01c3be482a3655a176f", size = 75544 }, - { url = "https://files.pythonhosted.org/packages/56/a8/dbe799f3620a08337ff5f3be27df7b5ba5beb1ee06acaf75f3cb46f8d650/simplejson-3.19.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:32a3ada8f3ea41db35e6d37b86dade03760f804628ec22e4fe775b703d567426", size = 75593 }, - { url = "https://files.pythonhosted.org/packages/d5/53/6ed299b9201ea914bb6a178a7e65413ed1969981533f50bfbe8a215be98f/simplejson-3.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f455672f4738b0f47183c5896e3606cd65c9ddee3805a4d18e8c96aa3f47c84", size = 138077 }, - { url = "https://files.pythonhosted.org/packages/1c/73/14306559157a6faedb4ecae28ad907b64b5359be5c9ec79233546acb96a4/simplejson-3.19.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b737a5fefedb8333fa50b8db3dcc9b1d18fd6c598f89fa7debff8b46bf4e511", size = 146307 }, - { url = "https://files.pythonhosted.org/packages/5b/1a/7994abb33e53ec972dd5e6dbb337b9070d3ad96017c4cff9d5dc83678ad4/simplejson-3.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb47ee773ce67476a960e2db4a0a906680c54f662521550828c0cc57d0099426", size = 133922 }, - { url = "https://files.pythonhosted.org/packages/08/15/8b4e1a8c7729b37797d0eab1381f517f928bd323d17efa7f4414c3565e1f/simplejson-3.19.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eed8cd98a7b24861da9d3d937f5fbfb6657350c547528a117297fe49e3960667", size = 137367 }, - { url = "https://files.pythonhosted.org/packages/59/9a/f5b786fe611395564d3e84f58f668242a7a2e674b4fac71b4e6b21d6d2b7/simplejson-3.19.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:619756f1dd634b5bdf57d9a3914300526c3b348188a765e45b8b08eabef0c94e", size = 139513 }, - { url = "https://files.pythonhosted.org/packages/4d/87/c310daf5e2f10306de3720f075f8ed74cbe83396879b8c55e832393233a5/simplejson-3.19.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dd7230d061e755d60a4d5445bae854afe33444cdb182f3815cff26ac9fb29a15", size = 139749 }, - { url = "https://files.pythonhosted.org/packages/fd/89/690880e1639b421a919d36fadf1fc364a38c3bc4f208dc11627426cdbe98/simplejson-3.19.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:101a3c8392028cd704a93c7cba8926594e775ca3c91e0bee82144e34190903f1", size = 148103 }, - { url = "https://files.pythonhosted.org/packages/a3/31/ef13eda5b5a0d8d9555b70151ee2956f63b845e1fac4ff904339dfb4dd89/simplejson-3.19.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e557712fc79f251673aeb3fad3501d7d4da3a27eff0857af2e1d1afbbcf6685", size = 140740 }, - { url = "https://files.pythonhosted.org/packages/39/5f/26b0a036592e45a2cb4be2f53d8827257e169bd5c84744a1aac89b0ff56f/simplejson-3.19.3-cp39-cp39-win32.whl", hash = "sha256:0bc5544e3128891bf613b9f71813ee2ec9c11574806f74dd8bb84e5e95bf64a2", size = 74115 }, - { url = "https://files.pythonhosted.org/packages/32/06/a35e2e1d8850aff1cf1320d4887bd5f97921c8964a1e260983d38d5d6c17/simplejson-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:06662392e4913dc8846d6a71a6d5de86db5fba244831abe1dd741d62a4136764", size = 75636 }, - { url = "https://files.pythonhosted.org/packages/0d/e7/f9fafbd4f39793a20cc52e77bbd766f7384312526d402c382928dc7667f6/simplejson-3.19.3-py3-none-any.whl", hash = "sha256:49cc4c7b940d43bd12bf87ec63f28cbc4964fc4e12c031cc8cd01650f43eb94e", size = 57004 }, -] - [[package]] name = "six" version = "1.17.0" @@ -1751,20 +1229,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] -[[package]] -name = "virtualenv" -version = "20.29.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, -] - [[package]] name = "watchfiles" version = "1.0.4" @@ -1947,59 +1411,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, ] -[[package]] -name = "zope-event" -version = "5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824 }, -] - -[[package]] -name = "zope-interface" -version = "7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243 }, - { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759 }, - { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922 }, - { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367 }, - { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488 }, - { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947 }, - { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776 }, - { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296 }, - { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997 }, - { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038 }, - { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806 }, - { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305 }, - { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959 }, - { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357 }, - { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235 }, - { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253 }, - { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702 }, - { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466 }, - { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961 }, - { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356 }, - { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196 }, - { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237 }, - { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696 }, - { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472 }, - { url = "https://files.pythonhosted.org/packages/8c/2c/1f49dc8b4843c4f0848d8e43191aed312bad946a1563d1bf9e46cf2816ee/zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb", size = 208349 }, - { url = "https://files.pythonhosted.org/packages/ed/7d/83ddbfc8424c69579a90fc8edc2b797223da2a8083a94d8dfa0e374c5ed4/zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7", size = 208799 }, - { url = "https://files.pythonhosted.org/packages/36/22/b1abd91854c1be03f5542fe092e6a745096d2eca7704d69432e119100583/zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137", size = 254267 }, - { url = "https://files.pythonhosted.org/packages/2a/dd/fcd313ee216ad0739ae00e6126bc22a0af62a74f76a9ca668d16cd276222/zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519", size = 248614 }, - { url = "https://files.pythonhosted.org/packages/88/d4/4ba1569b856870527cec4bf22b91fe704b81a3c1a451b2ccf234e9e0666f/zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75", size = 253800 }, - { url = "https://files.pythonhosted.org/packages/69/da/c9cfb384c18bd3a26d9fc6a9b5f32ccea49ae09444f097eaa5ca9814aff9/zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d", size = 211980 }, -] - [[package]] name = "zstandard" version = "0.23.0" From d3c053c01c7b4173c632e02cc8aea928b62eeee5 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 14:24:46 -0500 Subject: [PATCH 11/29] Fix pytest invocations --- hatch.toml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 hatch.toml diff --git a/hatch.toml b/hatch.toml new file mode 100644 index 0000000000..59c34aa09c --- /dev/null +++ b/hatch.toml @@ -0,0 +1,39 @@ +# See https://hatch.pypa.io/dev/config/environment/overview/ + +[envs.doc] +features = ["docs"] +[envs.doc.scripts] +build = "sphinx-build -W -b html doc ./doc/_build/html" +serve = "sphinx-autobuild -W -b html doc --watch ./pymongo --watch ./bson --watch ./gridfs ./doc/_build/serve" +linkcheck = "sphinx-build -E -b linkcheck doc ./doc/_build/linkcheck" + +[envs.doctest] +features = ["docs","test"] +[envs.doctest.scripts] +test = "sphinx-build -E -b doctest doc ./doc/_build/doctest" + +[envs.typing] +pre-install-commands = [ + "pip install -q -r requirements/typing.txt", +] +[envs.typing.scripts] +check-mypy = [ + "mypy --install-types --non-interactive bson gridfs tools pymongo", + "mypy --install-types --non-interactive --config-file mypy_test.ini test", + "mypy --install-types --non-interactive test/test_typing.py test/test_typing_strict.py" +] +check-pyright = ["rm -f pyrightconfig.json", "pyright test/test_typing.py test/test_typing_strict.py"] +check-strict-pyright = [ + "echo '{{\"strict\": [\"tests/test_typing_strict.py\"]}}' > pyrightconfig.json", + "pyright test/test_typing_strict.py", + "rm -f pyrightconfig.json" +] +check = ["check-mypy", "check-pyright", "check-strict-pyright"] + +[envs.test] +features = ["test"] +[envs.test.scripts] +test = "pytest -v --durations=5 --maxfail=10 {args}" +test-eg = "bash ./.evergreen/run-tests.sh {args}" +test-async = "pytest -v --durations=5 --maxfail=10 -m asyncio {args}" +test-mockupdb = ["pip install -U git+https://github.com/mongodb-labs/mongo-mockup-db@master", "test -m mockupdb"] From d2620be2e2e0f312a280d6b74085a90f18140747 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 14:50:48 -0500 Subject: [PATCH 12/29] Workflow updates for asyncio tests --- .evergreen/run-tests.sh | 2 ++ .github/workflows/test-python.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index d647955059..611e01a5ea 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -271,6 +271,8 @@ if [ -z "$GREEN_FRAMEWORK" ]; then fi # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS + PYTEST_ARGS="$PYTEST_ARGS -m asyncio" + uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS else # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 3760e308a5..b3c5902824 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -94,8 +94,10 @@ jobs: run: | if [[ "${{ matrix.python-version }}" == "3.13t" ]]; then pytest -v --durations=5 --maxfail=10 + pytest -v --durations=5 --maxfail=10 -m asyncio else just test + just test-async fi doctest: From 7ee03c9fb571e53efa9c3955a089fe41d9d5fe9f Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 14:59:45 -0500 Subject: [PATCH 13/29] Cleanup --- hatch.toml | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 hatch.toml diff --git a/hatch.toml b/hatch.toml deleted file mode 100644 index 59c34aa09c..0000000000 --- a/hatch.toml +++ /dev/null @@ -1,39 +0,0 @@ -# See https://hatch.pypa.io/dev/config/environment/overview/ - -[envs.doc] -features = ["docs"] -[envs.doc.scripts] -build = "sphinx-build -W -b html doc ./doc/_build/html" -serve = "sphinx-autobuild -W -b html doc --watch ./pymongo --watch ./bson --watch ./gridfs ./doc/_build/serve" -linkcheck = "sphinx-build -E -b linkcheck doc ./doc/_build/linkcheck" - -[envs.doctest] -features = ["docs","test"] -[envs.doctest.scripts] -test = "sphinx-build -E -b doctest doc ./doc/_build/doctest" - -[envs.typing] -pre-install-commands = [ - "pip install -q -r requirements/typing.txt", -] -[envs.typing.scripts] -check-mypy = [ - "mypy --install-types --non-interactive bson gridfs tools pymongo", - "mypy --install-types --non-interactive --config-file mypy_test.ini test", - "mypy --install-types --non-interactive test/test_typing.py test/test_typing_strict.py" -] -check-pyright = ["rm -f pyrightconfig.json", "pyright test/test_typing.py test/test_typing_strict.py"] -check-strict-pyright = [ - "echo '{{\"strict\": [\"tests/test_typing_strict.py\"]}}' > pyrightconfig.json", - "pyright test/test_typing_strict.py", - "rm -f pyrightconfig.json" -] -check = ["check-mypy", "check-pyright", "check-strict-pyright"] - -[envs.test] -features = ["test"] -[envs.test.scripts] -test = "pytest -v --durations=5 --maxfail=10 {args}" -test-eg = "bash ./.evergreen/run-tests.sh {args}" -test-async = "pytest -v --durations=5 --maxfail=10 -m asyncio {args}" -test-mockupdb = ["pip install -U git+https://github.com/mongodb-labs/mongo-mockup-db@master", "test -m mockupdb"] From b9d98c91f670aa06a91d58882d25d8223330022d Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 15:16:46 -0500 Subject: [PATCH 14/29] Typing fixes --- .evergreen/run-tests.sh | 1 + test/asynchronous/test_client.py | 16 ++++++++-------- test/test_client.py | 16 ++++++++-------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 611e01a5ea..0b56cea13a 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -272,6 +272,7 @@ if [ -z "$GREEN_FRAMEWORK" ]; then # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS PYTEST_ARGS="$PYTEST_ARGS -m asyncio" + # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS else # shellcheck disable=SC2048 diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 717a28712e..0f3330f56a 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -175,15 +175,15 @@ async def test_connect_timeout(self, simple_client): async def test_types(self): with pytest.raises(TypeError): - AsyncMongoClient(1) + AsyncMongoClient(1) # type: ignore[arg-type] with pytest.raises(TypeError): - AsyncMongoClient(1.14) + AsyncMongoClient(1.14) # type: ignore[arg-type] with pytest.raises(TypeError): - AsyncMongoClient("localhost", "27017") + AsyncMongoClient("localhost", "27017") # type: ignore[arg-type] with pytest.raises(TypeError): - AsyncMongoClient("localhost", 1.14) + AsyncMongoClient("localhost", 1.14) # type: ignore[arg-type] with pytest.raises(TypeError): - AsyncMongoClient("localhost", []) + AsyncMongoClient("localhost", []) # type: ignore[arg-type] with pytest.raises(ConfigurationError): AsyncMongoClient([]) @@ -389,11 +389,11 @@ async def test_metadata(self, simple_client): # Bad "driver" options. with pytest.raises(TypeError): - DriverInfo("Foo", 1, "a") + DriverInfo("Foo", 1, "a") # type: ignore[arg-type] with pytest.raises(TypeError): - DriverInfo(version="1", platform="a") + DriverInfo(version="1", platform="a") # type: ignore[call-arg] with pytest.raises(TypeError): - DriverInfo() + DriverInfo() # type: ignore[call-arg] with pytest.raises(TypeError): await simple_client(driver=1) with pytest.raises(TypeError): diff --git a/test/test_client.py b/test/test_client.py index d2f2c94cbe..c6175c2a6a 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -174,15 +174,15 @@ def test_connect_timeout(self, simple_client): def test_types(self): with pytest.raises(TypeError): - MongoClient(1) + MongoClient(1) # type: ignore[arg-type] with pytest.raises(TypeError): - MongoClient(1.14) + MongoClient(1.14) # type: ignore[arg-type] with pytest.raises(TypeError): - MongoClient("localhost", "27017") + MongoClient("localhost", "27017") # type: ignore[arg-type] with pytest.raises(TypeError): - MongoClient("localhost", 1.14) + MongoClient("localhost", 1.14) # type: ignore[arg-type] with pytest.raises(TypeError): - MongoClient("localhost", []) + MongoClient("localhost", []) # type: ignore[arg-type] with pytest.raises(ConfigurationError): MongoClient([]) @@ -371,11 +371,11 @@ def test_metadata(self, simple_client): # Bad "driver" options. with pytest.raises(TypeError): - DriverInfo("Foo", 1, "a") + DriverInfo("Foo", 1, "a") # type: ignore[arg-type] with pytest.raises(TypeError): - DriverInfo(version="1", platform="a") + DriverInfo(version="1", platform="a") # type: ignore[call-arg] with pytest.raises(TypeError): - DriverInfo() + DriverInfo() # type: ignore[call-arg] with pytest.raises(TypeError): simple_client(driver=1) with pytest.raises(TypeError): From 41fe61e948003e20730d801e0a03179f25103950 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 15:31:47 -0500 Subject: [PATCH 15/29] Fix supports_secondary_read_pref --- test/__init__.py | 2 +- test/asynchronous/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index d6aaf123c2..8cd73009df 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -596,7 +596,7 @@ def supports_secondary_read_pref(self): if self.has_secondaries: return True if self.is_mongos: - shard = self.client.config.shards.find_one()["host"] # type:ignore[index] + shard = (self.client.config.shards.find_one())["host"] # type:ignore[index] num_members = shard.count(",") + 1 return num_members > 1 return False diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 616137cfe9..13cfbe1d4c 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -598,7 +598,7 @@ async def supports_secondary_read_pref(self): if await self.has_secondaries: return True if self.is_mongos: - shard = await self.client.config.shards.find_one()["host"] # type:ignore[index] + shard = (await self.client.config.shards.find_one())["host"] # type:ignore[index] num_members = shard.count(",") + 1 return num_members > 1 return False From 47b8c9d3393c6ccab49cde8f76253109393507ea Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 15:35:58 -0500 Subject: [PATCH 16/29] Fix async pytest invocation for EG --- .evergreen/run-tests.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 0b56cea13a..a329e0f84b 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -271,9 +271,13 @@ if [ -z "$GREEN_FRAMEWORK" ]; then fi # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS - PYTEST_ARGS="$PYTEST_ARGS -m asyncio" - # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS + + # Workaround until unittest -> pytest conversion is complete + if [ -z "$TEST_SUITES" ]; then + PYTEST_ARGS="$PYTEST_ARGS -m asyncio" + # shellcheck disable=SC2048 + uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS + fi else # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS From 9e115bef1d09d7603f4c7b067701e525426b08ce Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 16:43:09 -0500 Subject: [PATCH 17/29] Remove executor closes --- .evergreen/run-tests.sh | 7 ++++++- pymongo/asynchronous/mongo_client.py | 2 -- pymongo/asynchronous/monitor.py | 4 ---- pymongo/synchronous/mongo_client.py | 2 -- pymongo/synchronous/monitor.py | 4 ---- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index a329e0f84b..cf5839e93a 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -274,10 +274,15 @@ if [ -z "$GREEN_FRAMEWORK" ]; then # Workaround until unittest -> pytest conversion is complete if [ -z "$TEST_SUITES" ]; then - PYTEST_ARGS="$PYTEST_ARGS -m asyncio" + PYTEST_ARGS="-m asyncio --junitxml=xunit-results/TEST-asyncresults.xml $PYTEST_ARGS" + # shellcheck disable=SC2048 + uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS + else + PYTEST_ARGS="-m $TEST_SUITES and asyncio --junitxml=xunit-results/TEST-asyncresults.xml $PYTEST_ARGS" # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS fi + else # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index bf44a776ca..40d527928f 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1561,8 +1561,6 @@ async def close(self) -> None: # Stop the periodic task thread and then send pending killCursor # requests before closing the topology. self._kill_cursors_executor.close() - if not _IS_SYNC: - await self._kill_cursors_executor.join() await self._process_kill_cursors() await self._topology.close() if self._encrypter: diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index de22a30780..ad1bc70aba 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -191,8 +191,6 @@ def gc_safe_close(self) -> None: async def close(self) -> None: self.gc_safe_close() - if not _IS_SYNC: - await self._executor.join() await self._rtt_monitor.close() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. @@ -460,8 +458,6 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool async def close(self) -> None: self.gc_safe_close() - if not _IS_SYNC: - await self._executor.join() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. await self._pool.reset() diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 65c920f3d5..da4216cd31 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -1555,8 +1555,6 @@ def close(self) -> None: # Stop the periodic task thread and then send pending killCursor # requests before closing the topology. self._kill_cursors_executor.close() - if not _IS_SYNC: - self._kill_cursors_executor.join() self._process_kill_cursors() self._topology.close() if self._encrypter: diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index 5558c5fd07..df4130d4ab 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -191,8 +191,6 @@ def gc_safe_close(self) -> None: def close(self) -> None: self.gc_safe_close() - if not _IS_SYNC: - self._executor.join() self._rtt_monitor.close() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. @@ -460,8 +458,6 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool def close(self) -> None: self.gc_safe_close() - if not _IS_SYNC: - self._executor.join() # Increment the generation and maybe close the socket. If the executor # thread has the socket checked out, it will be closed when checked in. self._pool.reset() From 8602ddeab39a122349291b920e460db7169207be Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 23 Jan 2025 16:44:36 -0500 Subject: [PATCH 18/29] Remove executor cancel from close --- pymongo/periodic_executor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pymongo/periodic_executor.py b/pymongo/periodic_executor.py index f51a988728..9b10f6e7e3 100644 --- a/pymongo/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -75,8 +75,6 @@ def close(self, dummy: Any = None) -> None: callback; see monitor.py. """ self._stopped = True - if self._task is not None: - self._task.cancel() async def join(self, timeout: Optional[int] = None) -> None: if self._task is not None: From cc3f1ad4a43e77bb45d879cc736ea5d54a3554cd Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 11:18:28 -0500 Subject: [PATCH 19/29] run-tests.sh fix for async --- .evergreen/run-tests.sh | 15 +++++++-------- pymongo/asynchronous/mongo_client.py | 1 + pymongo/synchronous/mongo_client.py | 1 + test/asynchronous/test_client.py | 2 +- test/test_client.py | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index cf5839e93a..7c93f21ecd 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -267,6 +267,9 @@ if [ -z "$GREEN_FRAMEWORK" ]; then # https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html PYTEST_ARGS="-v --capture=tee-sys --durations=5 $TEST_ARGS" if [ -n "$TEST_SUITES" ]; then + # Workaround until unittest -> pytest conversion is complete + # shellcheck disable=SC2206 + ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) PYTEST_ARGS="-m $TEST_SUITES $PYTEST_ARGS" fi # shellcheck disable=SC2048 @@ -274,15 +277,11 @@ if [ -z "$GREEN_FRAMEWORK" ]; then # Workaround until unittest -> pytest conversion is complete if [ -z "$TEST_SUITES" ]; then - PYTEST_ARGS="-m asyncio --junitxml=xunit-results/TEST-asyncresults.xml $PYTEST_ARGS" - # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS - else - PYTEST_ARGS="-m $TEST_SUITES and asyncio --junitxml=xunit-results/TEST-asyncresults.xml $PYTEST_ARGS" - # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS + # shellcheck disable=SC2206 + ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) fi - + # shellcheck disable=SC2048 + uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" else # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 40d527928f..2f549c6f3c 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1420,6 +1420,7 @@ def __next__(self) -> NoReturn: next = __next__ if not _IS_SYNC: anext = next + __anext__ = next async def _server_property(self, attr_name: str) -> Any: """An attribute of the current server's description. diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index da4216cd31..a199e0ea2d 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -1416,6 +1416,7 @@ def __next__(self) -> NoReturn: next = __next__ if not _IS_SYNC: next = next + __next__ = next def _server_property(self, attr_name: str) -> Any: """An attribute of the current server's description. diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 0f3330f56a..b2902371e7 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -243,7 +243,7 @@ async def test_getattr(self, async_client): assert "has no attribute '_does_not_exist'" in str(context.value) async def test_iteration(self, async_client): - if _IS_SYNC: + if _IS_SYNC or sys.version_info < (3, 10): msg = "'AsyncMongoClient' object is not iterable" else: msg = "'AsyncMongoClient' object is not an async iterator" diff --git a/test/test_client.py b/test/test_client.py index c6175c2a6a..da2fe363ab 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -240,7 +240,7 @@ def test_getattr(self, client): assert "has no attribute '_does_not_exist'" in str(context.value) def test_iteration(self, client): - if _IS_SYNC: + if _IS_SYNC or sys.version_info < (3, 10): msg = "'MongoClient' object is not iterable" else: msg = "'MongoClient' object is not an async iterator" From d906c3b39c963c729a74a385a489dc99933633ea Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 11:21:43 -0500 Subject: [PATCH 20/29] Fix uv.lock --- uv.lock | 617 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 603 insertions(+), 14 deletions(-) diff --git a/uv.lock b/uv.lock index 8d3d9e2dcc..e7f09f66fc 100644 --- a/uv.lock +++ b/uv.lock @@ -181,6 +181,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + [[package]] name = "charset-normalizer" version = "3.4.1" @@ -260,7 +269,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -276,6 +285,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/d3/3ec80acdd57a0d6a1111b978ade388824f37126446fd6750d38bfaca949c/coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8", size = 798314 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/db/08d54dbc12fdfe5857b06105fd1235bdebb7da7c11cd1a0fae936556162a/coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c", size = 210025 }, + { url = "https://files.pythonhosted.org/packages/a8/ff/02c4bcff1025b4a788aa3933e1cd1474d79de43e0d859273b3319ef43cd3/coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b", size = 210499 }, + { url = "https://files.pythonhosted.org/packages/ab/b1/7820a8ef62adeebd37612af9d2369f4467a3bc2641dea1243450def5489e/coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932", size = 238399 }, + { url = "https://files.pythonhosted.org/packages/2c/0e/23a388f3ce16c5ea01a454fef6a9039115abd40b748027d4fef18b3628a7/coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3", size = 236676 }, + { url = "https://files.pythonhosted.org/packages/f8/81/e871b0d58ca5d6cc27d00b2f668ce09c4643ef00512341f3a592a81fb6cd/coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517", size = 237467 }, + { url = "https://files.pythonhosted.org/packages/95/cb/42a6d34d5840635394f1e172aaa0e7cbd9346155e5004a8ee75d8e434c6b/coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a", size = 243539 }, + { url = "https://files.pythonhosted.org/packages/6a/6a/18b3819919fdfd3e2062a75219b363f895f24ae5b80e72ffe5dfb1a7e9c8/coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880", size = 241725 }, + { url = "https://files.pythonhosted.org/packages/b5/3d/a0650978e8b8f78d269358421b7401acaf7cb89e957b2e1be5205ea5940e/coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58", size = 242913 }, + { url = "https://files.pythonhosted.org/packages/8a/fe/95a74158fa0eda56d39783e918edc6fbb3dd3336be390557fc0a2815ecd4/coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4", size = 212381 }, + { url = "https://files.pythonhosted.org/packages/4c/26/b276e0c70cba5059becce2594a268a2731d5b4f2386e9a6afdf37ffa3d44/coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a", size = 213225 }, + { url = "https://files.pythonhosted.org/packages/71/cf/964bb667ea37d64b25f04d4cfaf6232cdb7a6472e1f4a4faf0459ddcec40/coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375", size = 210130 }, + { url = "https://files.pythonhosted.org/packages/aa/56/31edd4baa132fe2b991437e0acf3e36c50418370044a89b65518e5581f4c/coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb", size = 210617 }, + { url = "https://files.pythonhosted.org/packages/26/6d/4cd14bd0221180c307fae4f8ef00dbd86a13507c25081858c620aa6fafd8/coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95", size = 242048 }, + { url = "https://files.pythonhosted.org/packages/84/60/7eb84255bd9947b140e0382721b0a1b25fd670b4f0f176f11f90b5632d02/coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d", size = 239619 }, + { url = "https://files.pythonhosted.org/packages/76/6b/e8f4696194fdf3c19422f2a80ac10e03a9322f93e6c9ef57a89e03a8c8f7/coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743", size = 241321 }, + { url = "https://files.pythonhosted.org/packages/3f/1c/6a6990fd2e6890807775852882b1ed0a8e50519a525252490b0c219aa8a5/coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1", size = 250419 }, + { url = "https://files.pythonhosted.org/packages/1a/be/b6422a1422381704dd015cc23e503acd1a44a6bdc4e59c75f8c6a2b24151/coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de", size = 248794 }, + { url = "https://files.pythonhosted.org/packages/9b/93/e8231000754d4a31fe9a6c550f6a436eacd2e50763ba2b418f10b2308e45/coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff", size = 249873 }, + { url = "https://files.pythonhosted.org/packages/d3/6f/eb5aae80bf9d01d0f293121d4caa660ac968da2cb967f82547a7b5e8d65b/coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d", size = 212380 }, + { url = "https://files.pythonhosted.org/packages/30/73/b70ab57f11b62f5ca9a83f43cae752fbbb4417bea651875235c32eb2fc2e/coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656", size = 213316 }, + { url = "https://files.pythonhosted.org/packages/36/db/f4e17ffb5ac2d125c72ee3b235c2e04f85a4296a6a9e17730e218af113d8/coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9", size = 210340 }, + { url = "https://files.pythonhosted.org/packages/c3/bc/d7e832280f269be9e8d46cff5c4031b4840f1844674dc53ad93c5a9c1da6/coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64", size = 210612 }, + { url = "https://files.pythonhosted.org/packages/54/84/543e2cd6c1de30c7522a0afcb040677957bac756dd8677bade8bdd9274ba/coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af", size = 242926 }, + { url = "https://files.pythonhosted.org/packages/ad/06/570533f747141b4fd727a193317e16c6e677ed7945e23a195b8f64e685a2/coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc", size = 240294 }, + { url = "https://files.pythonhosted.org/packages/fa/d9/ec4ba0913195d240d026670d41b91f3e5b9a8a143a385f93a09e97c90f5c/coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2", size = 242232 }, + { url = "https://files.pythonhosted.org/packages/d9/3f/1a613c32aa1980d20d6ca2f54faf800df04aafad6016d7132b3276d8715d/coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1", size = 249171 }, + { url = "https://files.pythonhosted.org/packages/b9/3b/e16b12693572fd69148453abc6ddcd20cbeae6f0a040b5ed6af2f75b646f/coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb", size = 247073 }, + { url = "https://files.pythonhosted.org/packages/e7/3e/04a05d40bb09f90a312296a32fb2c5ade2dfcf803edf777ad18b97547503/coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2", size = 248812 }, + { url = "https://files.pythonhosted.org/packages/ba/f7/3a8b7b0affe548227f3d45e248c0f22c5b55bff0ee062b49afc165b3ff25/coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4", size = 212634 }, + { url = "https://files.pythonhosted.org/packages/7c/31/5f5286d2a5e21e1fe5670629bb24c79bf46383a092e74e00077e7a178e5c/coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475", size = 213460 }, + { url = "https://files.pythonhosted.org/packages/62/18/5573216d5b8db7d9f29189350dcd81830a03a624966c35f8201ae10df09c/coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1", size = 210014 }, + { url = "https://files.pythonhosted.org/packages/7c/0e/e98d6c6d569d65ff3195f095e6b006b3d7780fd6182322a25e7dfe0d53d3/coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5", size = 210494 }, + { url = "https://files.pythonhosted.org/packages/d3/63/98e5a6b7ed1bfca874729ee309cc49a6d6658ab9e479a2b6d223ccc96e03/coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631", size = 237996 }, + { url = "https://files.pythonhosted.org/packages/76/e4/d3c67a0a092127b8a3dffa2f75334a8cdb2cefc99e3d75a7f42cf1ff98a9/coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46", size = 236287 }, + { url = "https://files.pythonhosted.org/packages/12/7f/9b787ffc31bc39aa9e98c7005b698e7c6539bd222043e4a9c83b83c782a2/coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e", size = 237070 }, + { url = "https://files.pythonhosted.org/packages/31/ee/9998a0d855cad5f8e04062f7428b83c34aa643e5df468409593a480d5585/coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be", size = 243115 }, + { url = "https://files.pythonhosted.org/packages/16/94/1e348cd4445404c588ec8199adde0b45727b1d7989d8fb097d39c93e3da5/coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b", size = 241315 }, + { url = "https://files.pythonhosted.org/packages/28/17/6fe1695d2a706e586b87a407598f4ed82dd218b2b43cdc790f695f259849/coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0", size = 242467 }, + { url = "https://files.pythonhosted.org/packages/81/a2/1e550272c8b1f89b980504230b1a929de83d8f3d5ecb268477b32e5996a6/coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7", size = 212394 }, + { url = "https://files.pythonhosted.org/packages/c9/48/7d3c31064c5adcc743fe5370cf7e198cee06cc0e2d37b5cbe930691a3f54/coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493", size = 213246 }, + { url = "https://files.pythonhosted.org/packages/34/81/f00ce7ef95479085feb01fa9e352b2b5b2b9d24767acf2266d6267a6dba9/coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067", size = 202381 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cramjam" version = "2.9.1" @@ -405,6 +468,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + [[package]] name = "dnspython" version = "2.7.0" @@ -423,6 +495,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] +[[package]] +name = "eventlet" +version = "0.38.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "greenlet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/4e/f974cc85b8d19b31176e0cca90e1650156f385c9c294a96fc42846ca75e9/eventlet-0.38.2.tar.gz", hash = "sha256:6a46823af1dca7d29cf04c0d680365805435473c3acbffc176765c7f8787edac", size = 561526 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/07/00feb2c708d71796e190a3051a0d530a4922bfb6b346aa8302725840698c/eventlet-0.38.2-py3-none-any.whl", hash = "sha256:4a2e3cbc53917c8f39074ccf689501168563d3a4df59e9cddd5e9d3b7f85c599", size = 363192 }, +] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -432,6 +517,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + [[package]] name = "furo" version = "2024.8.6" @@ -448,6 +542,118 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, ] +[[package]] +name = "gevent" +version = "24.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/75/a53f1cb732420f5e5d79b2563fc3504d22115e7ecfe7966e5cf9b3582ae7/gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca", size = 5976624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/7d/27ed3603f4bf96b36fb2746e923e033bc600c6684de8fe164d64eb8c4dcc/gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e", size = 2998254 }, + { url = "https://files.pythonhosted.org/packages/a8/03/a8f6c70f50a644a79e75d9f15e6f1813115d34c3c55528e4669a9316534d/gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870", size = 4817711 }, + { url = "https://files.pythonhosted.org/packages/f0/05/4f9bc565520a18f107464d40ac15a91708431362c797e77fbb5e7ff26e64/gevent-24.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7398c629d43b1b6fd785db8ebd46c0a353880a6fab03d1cf9b6788e7240ee32e", size = 4934468 }, + { url = "https://files.pythonhosted.org/packages/4a/7d/f15561eeebecbebc0296dd7bebea10ac4af0065d98249e3d8c4998e68edd/gevent-24.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7886b63ebfb865178ab28784accd32f287d5349b3ed71094c86e4d3ca738af5", size = 5014067 }, + { url = "https://files.pythonhosted.org/packages/67/c1/07eff117a600fc3c9bd4e3a1ff3b726f146ee23ce55981156547ccae0c85/gevent-24.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ca80711e6553880974898d99357fb649e062f9058418a92120ca06c18c3c59", size = 6625531 }, + { url = "https://files.pythonhosted.org/packages/4b/72/43f76ab6b18e5e56b1003c844829971f3044af08b39b3c9040559be00a2b/gevent-24.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e24181d172f50097ac8fc272c8c5b030149b630df02d1c639ee9f878a470ba2b", size = 5249671 }, + { url = "https://files.pythonhosted.org/packages/6b/fc/1a847ada0757cc7690f83959227514b1a52ff6de504619501c81805fa1da/gevent-24.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1d4fadc319b13ef0a3c44d2792f7918cf1bca27cacd4d41431c22e6b46668026", size = 6773903 }, + { url = "https://files.pythonhosted.org/packages/3b/9d/254dcf455f6659ab7e36bec0bc11f51b18ea25eac2de69185e858ccf3c30/gevent-24.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d882faa24f347f761f934786dde6c73aa6c9187ee710189f12dcc3a63ed4a50", size = 1560443 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/86a170f77ef51a15297573c50dbec4cc67ddc98b677cc2d03cc7f2927f4c/gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62", size = 2951424 }, + { url = "https://files.pythonhosted.org/packages/7f/0a/987268c9d446f61883bc627c77c5ed4a97869c0f541f76661a62b2c411f6/gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab", size = 4878504 }, + { url = "https://files.pythonhosted.org/packages/dc/d4/2f77ddd837c0e21b4a4460bcb79318b6754d95ef138b7a29f3221c7e9993/gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758", size = 5007668 }, + { url = "https://files.pythonhosted.org/packages/80/a0/829e0399a1f9b84c344b72d2be9aa60fe2a64e993cac221edcc14f069679/gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d", size = 5067055 }, + { url = "https://files.pythonhosted.org/packages/1e/67/0e693f9ddb7909c2414f8fcfc2409aa4157884c147bc83dab979e9cf717c/gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546", size = 6761883 }, + { url = "https://files.pythonhosted.org/packages/fa/b6/b69883fc069d7148dd23c5dda20826044e54e7197f3c8e72b8cc2cd4035a/gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c", size = 5440802 }, + { url = "https://files.pythonhosted.org/packages/32/4e/b00094d995ff01fd88b3cf6b9d1d794f935c31c645c431e65cd82d808c9c/gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61", size = 6866992 }, + { url = "https://files.pythonhosted.org/packages/37/ed/58dbe9fb09d36f6477ff8db0459ebd3be9a77dc05ae5d96dc91ad657610d/gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897", size = 1543736 }, + { url = "https://files.pythonhosted.org/packages/dd/32/301676f67ffa996ff1c4175092fb0c48c83271cc95e5c67650b87156b6cf/gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46", size = 2956467 }, + { url = "https://files.pythonhosted.org/packages/6b/84/aef1a598123cef2375b6e2bf9d17606b961040f8a10e3dcc3c3dd2a99f05/gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb", size = 5136486 }, + { url = "https://files.pythonhosted.org/packages/92/7b/04f61187ee1df7a913b3fca63b0a1206c29141ab4d2a57e7645237b6feb5/gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85", size = 5299718 }, + { url = "https://files.pythonhosted.org/packages/36/2a/ebd12183ac25eece91d084be2111e582b061f4d15ead32239b43ed47e9ba/gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6", size = 5400118 }, + { url = "https://files.pythonhosted.org/packages/ec/c9/f006c0cd59f0720fbb62ee11da0ad4c4c0fd12799afd957dd491137e80d9/gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671", size = 6775163 }, + { url = "https://files.pythonhosted.org/packages/49/f1/5edf00b674b10d67e3b967c2d46b8a124c2bc8cfd59d4722704392206444/gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f", size = 5479886 }, + { url = "https://files.pythonhosted.org/packages/22/11/c48e62744a32c0d48984268ae62b99edb81eaf0e03b42de52e2f09855509/gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a", size = 6891452 }, + { url = "https://files.pythonhosted.org/packages/11/b2/5d20664ef6a077bec9f27f7a7ee761edc64946d0b1e293726a3d074a9a18/gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae", size = 1541631 }, + { url = "https://files.pythonhosted.org/packages/a4/8f/4958e70caeaf469c576ecc5b5f2cb49ddaad74336fa82363d89cddb3c284/gevent-24.11.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d618e118fdb7af1d6c1a96597a5cd6ac84a9f3732b5be8515c6a66e098d498b6", size = 2949601 }, + { url = "https://files.pythonhosted.org/packages/3b/64/79892d250b7b2aa810688dfebe783aec02568e5cecacb1e100acbb9d95c6/gevent-24.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2142704c2adce9cd92f6600f371afb2860a446bfd0be5bd86cca5b3e12130766", size = 5107052 }, + { url = "https://files.pythonhosted.org/packages/66/44/9ee0ed1909b4f41375e32bf10036d5d8624962afcbd901573afdecd2e36a/gevent-24.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92e0d7759de2450a501effd99374256b26359e801b2d8bf3eedd3751973e87f5", size = 5271736 }, + { url = "https://files.pythonhosted.org/packages/e3/48/0184b2622a388a256199c5fadcad6b52b6455019c2a4b19edd6de58e30ba/gevent-24.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca845138965c8c56d1550499d6b923eb1a2331acfa9e13b817ad8305dde83d11", size = 5367782 }, + { url = "https://files.pythonhosted.org/packages/9a/b1/1a2704c346234d889d2e0042efb182534f7d294115f0e9f99d8079fa17eb/gevent-24.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:356b73d52a227d3313f8f828025b665deada57a43d02b1cf54e5d39028dbcf8d", size = 6757533 }, + { url = "https://files.pythonhosted.org/packages/ed/6e/b2eed8dec617264f0046d50a13a42d3f0a06c50071b9fc1eae00285a03f1/gevent-24.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:58851f23c4bdb70390f10fc020c973ffcf409eb1664086792c8b1e20f25eef43", size = 5449436 }, + { url = "https://files.pythonhosted.org/packages/63/c2/eca6b95fbf9af287fa91c327494e4b74a8d5bfa0156cd87b233f63f118dc/gevent-24.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1ea50009ecb7f1327347c37e9eb6561bdbc7de290769ee1404107b9a9cba7cf1", size = 6866470 }, + { url = "https://files.pythonhosted.org/packages/b7/e6/51824bd1f2c1ce70aa01495aa6ffe04ab789fa819fa7e6f0ad2388fb03c6/gevent-24.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:ec68e270543ecd532c4c1d70fca020f90aa5486ad49c4f3b8b2e64a66f5c9274", size = 1540088 }, + { url = "https://files.pythonhosted.org/packages/a0/73/263d0f63186d27d205b3dc157efe838afe3aba10a3baca15d85e97b90eae/gevent-24.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9347690f4e53de2c4af74e62d6fabc940b6d4a6cad555b5a379f61e7d3f2a8e", size = 6658480 }, + { url = "https://files.pythonhosted.org/packages/8a/fd/ec7b5c764a3d1340160b82f7394fdc1220d18e11ae089c472cf7bcc2fe6a/gevent-24.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8619d5c888cb7aebf9aec6703e410620ef5ad48cdc2d813dd606f8aa7ace675f", size = 6808247 }, + { url = "https://files.pythonhosted.org/packages/95/82/2ce68dc8dbc2c3ed3f4e73f21e1b7a45d80b5225670225a48e695f248850/gevent-24.11.1-cp39-cp39-win32.whl", hash = "sha256:c6b775381f805ff5faf250e3a07c0819529571d19bb2a9d474bee8c3f90d66af", size = 1483133 }, + { url = "https://files.pythonhosted.org/packages/76/96/aa4cbcf1807187b65a9c9ff15b32b08c2014968be852dda34d212cf8cc58/gevent-24.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c3443b0ed23dcb7c36a748d42587168672953d368f2956b17fad36d43b58836", size = 1566354 }, + { url = "https://files.pythonhosted.org/packages/86/63/197aa67250943b508b34995c2aa6b46402e7e6f11785487740c2057bfb20/gevent-24.11.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:f43f47e702d0c8e1b8b997c00f1601486f9f976f84ab704f8f11536e3fa144c9", size = 1271676 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, + { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, + { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, + { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, + { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, + { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, + { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, + { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, + { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, + { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -485,6 +691,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] +[[package]] +name = "identify" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 }, +] + [[package]] name = "idna" version = "3.10" @@ -613,6 +828,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, ] +[[package]] +name = "mockupdb" +version = "1.9.0.dev1" +source = { git = "https://github.com/mongodb-labs/mongo-mockup-db?rev=master#317c4e049965f9d99423698a81e52d0ab37b7599" } +dependencies = [ + { name = "pymongo" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493 }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702 }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104 }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167 }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834 }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "packaging" version = "24.2" @@ -622,6 +907,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pip" +version = "24.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -631,6 +934,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -698,7 +1017,7 @@ docs = [ { name = "sphinxcontrib-shellcheck" }, ] encryption = [ - { name = "certifi", marker = "sys_platform == 'darwin' or os_name == 'nt'" }, + { name = "certifi", marker = "os_name == 'nt' or sys_platform == 'darwin'" }, { name = "pymongo-auth-aws" }, { name = "pymongocrypt" }, ] @@ -707,7 +1026,7 @@ gssapi = [ { name = "winkerberos", marker = "os_name == 'nt'" }, ] ocsp = [ - { name = "certifi", marker = "sys_platform == 'darwin' or os_name == 'nt'" }, + { name = "certifi", marker = "os_name == 'nt' or sys_platform == 'darwin'" }, { name = "cryptography" }, { name = "pyopenssl" }, { name = "requests" }, @@ -724,10 +1043,40 @@ zstd = [ { name = "zstandard" }, ] +[package.dev-dependencies] +coverage = [ + { name = "coverage" }, + { name = "pytest-cov" }, +] +dev = [ + { name = "pre-commit" }, +] +eventlet = [ + { name = "eventlet" }, +] +gevent = [ + { name = "gevent" }, +] +mockupdb = [ + { name = "mockupdb" }, +] +perf = [ + { name = "simplejson" }, +] +pymongocrypt-source = [ + { name = "pymongocrypt" }, +] +typing = [ + { name = "mypy" }, + { name = "pip" }, + { name = "pyright" }, + { name = "typing-extensions" }, +] + [package.metadata] requires-dist = [ - { name = "certifi", marker = "(sys_platform == 'darwin' and extra == 'encryption') or (os_name == 'nt' and extra == 'encryption')" }, - { name = "certifi", marker = "(sys_platform == 'darwin' and extra == 'ocsp') or (os_name == 'nt' and extra == 'ocsp')" }, + { name = "certifi", marker = "(os_name == 'nt' and extra == 'encryption') or (sys_platform == 'darwin' and extra == 'encryption')" }, + { name = "certifi", marker = "(os_name == 'nt' and extra == 'ocsp') or (sys_platform == 'darwin' and extra == 'ocsp')" }, { name = "cryptography", marker = "extra == 'ocsp'", specifier = ">=2.5" }, { name = "dnspython", specifier = ">=1.16.0,<3.0.0" }, { name = "furo", marker = "extra == 'docs'", specifier = "==2024.8.6" }, @@ -750,6 +1099,24 @@ requires-dist = [ { name = "zstandard", marker = "extra == 'zstd'" }, ] +[package.metadata.requires-dev] +coverage = [ + { name = "coverage", specifier = ">=5,<=7.5" }, + { name = "pytest-cov" }, +] +dev = [{ name = "pre-commit", specifier = ">=4.0" }] +eventlet = [{ name = "eventlet" }] +gevent = [{ name = "gevent" }] +mockupdb = [{ name = "mockupdb", git = "https://github.com/mongodb-labs/mongo-mockup-db?rev=master" }] +perf = [{ name = "simplejson" }] +pymongocrypt-source = [{ name = "pymongocrypt", git = "https://github.com/mongodb/libmongocrypt?subdirectory=bindings%2Fpython&rev=master" }] +typing = [ + { name = "mypy", specifier = "==1.14.1" }, + { name = "pip" }, + { name = "pyright", specifier = "==1.1.392.post0" }, + { name = "typing-extensions" }, +] + [[package]] name = "pymongo-auth-aws" version = "1.3.0" @@ -765,21 +1132,14 @@ wheels = [ [[package]] name = "pymongocrypt" -version = "1.12.2" -source = { registry = "https://pypi.org/simple" } +version = "1.13.0.dev0" +source = { git = "https://github.com/mongodb/libmongocrypt?subdirectory=bindings%2Fpython&rev=master#90476d5db7737bab2ce1c198df5671a12dbaae1a" } dependencies = [ { name = "cffi" }, { name = "cryptography" }, { name = "httpx" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/79/8d30f1b4bd0fcf00f3433d8bb23cec65409fadaf8a0fe69e35b72e0d5aef/pymongocrypt-1.12.2.tar.gz", hash = "sha256:63214edf14274e19a3b0ba6d0e37067c2bb9714b15a891c08c239d2592a3f441", size = 62081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/4e/9f08034e4e7e208747150314d95d9cb572d52a72507f293d9913dac04bd4/pymongocrypt-1.12.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3b30fe6fd71ba9550318f6f7520454c0cef7e0505e1c30e962628c8e0e23a728", size = 4685815 }, - { url = "https://files.pythonhosted.org/packages/f0/07/cc7190d1ef5b23789724079bb91a4862d1a79fb31551d26f463eeb40faf4/pymongocrypt-1.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:755ca20a1f4fbd7e22a79ad4a774647e6cc0a48ebbe6458098ef23fc8e214987", size = 3736188 }, - { url = "https://files.pythonhosted.org/packages/d4/5a/f5f707e0a364261de8056237a65b6589e0d5324ef16084212a6429973f2d/pymongocrypt-1.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8888d950934ca1169fd85bb11df84edf994ccd560568bda86d1cb708414b3e88", size = 3497068 }, - { url = "https://files.pythonhosted.org/packages/c3/17/d2da8cb9b2421aade365abd18f55b91fc220a621dd4387fa6b79d7e6c606/pymongocrypt-1.12.2-py3-none-win_amd64.whl", hash = "sha256:bb0bfb8753e5c43cebe12754e2fc292e64ef6fc1cada4357b58f7afae91cc47a", size = 1556272 }, -] [[package]] name = "pyopenssl" @@ -794,6 +1154,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/d7/eb76863d2060dcbe7c7e6cccfd95ac02ea0b9acc37745a0d99ff6457aefb/pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90", size = 56453 }, ] +[[package]] +name = "pyright" +version = "1.1.392.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/df/3c6f6b08fba7ccf49b114dfc4bb33e25c299883fd763f93fad47ef8bc58d/pyright-1.1.392.post0.tar.gz", hash = "sha256:3b7f88de74a28dcfa90c7d90c782b6569a48c2be5f9d4add38472bdaac247ebd", size = 3789911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/b1/a18de17f40e4f61ca58856b9ef9b0febf74ff88978c3f7776f910071f567/pyright-1.1.392.post0-py3-none-any.whl", hash = "sha256:252f84458a46fa2f0fd4e2f91fc74f50b9ca52c757062e93f6c250c0d8329eb2", size = 5595487 }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -823,6 +1196,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/d8/defa05ae50dcd6019a95527200d3b3980043df5aa445d40cb0ef9f7f98ab/pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075", size = 19400 }, ] +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -847,6 +1233,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/c1/0ee413ddd639aebf22c85d6db39f136ccc10e6a4b4dd275a92b5c839de8d/python_snappy-0.7.3-py3-none-any.whl", hash = "sha256:074c0636cfcd97e7251330f428064050ac81a52c62ed884fc2ddebbb60ed7f50", size = 9155 }, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, +] + [[package]] name = "readthedocs-sphinx-search" version = "0.3.2" @@ -899,6 +1338,89 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 }, ] +[[package]] +name = "setuptools" +version = "75.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, +] + +[[package]] +name = "simplejson" +version = "3.19.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/29/085111f19717f865eceaf0d4397bf3e76b08d60428b076b64e2a1903706d/simplejson-3.19.3.tar.gz", hash = "sha256:8e086896c36210ab6050f2f9f095a5f1e03c83fa0e7f296d6cba425411364680", size = 85237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/24/260ad03435ce8ef2436031951134659c7161776ec3a78094b35b9375ceea/simplejson-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50d8b742d74c449c4dcac570d08ce0f21f6a149d2d9cf7652dbf2ba9a1bc729a", size = 93660 }, + { url = "https://files.pythonhosted.org/packages/63/a1/dee207f357bcd6b106f2ca5129ee916c24993ba08b7dfbf9a37c22442ea9/simplejson-3.19.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd011fc3c1d88b779645495fdb8189fb318a26981eebcce14109460e062f209b", size = 75546 }, + { url = "https://files.pythonhosted.org/packages/80/7b/45ef1da43f54d209ce2ef59b7356cda13f810186c381f38ae23a4d2b1337/simplejson-3.19.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:637c4d4b81825c1f4d651e56210bd35b5604034b192b02d2d8f17f7ce8c18f42", size = 75602 }, + { url = "https://files.pythonhosted.org/packages/7f/4b/9a132382982f8127bc7ce5212a5585d83c174707c9dd698d0cb6a0d41882/simplejson-3.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f56eb03bc9e432bb81adc8ecff2486d39feb371abb442964ffb44f6db23b332", size = 138632 }, + { url = "https://files.pythonhosted.org/packages/76/37/012f5ad2f38afa28f8a6ad9da01dc0b64492ffbaf2a3f2f8a0e1fddf9c1d/simplejson-3.19.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef59a53be400c1fad2c914b8d74c9d42384fed5174f9321dd021b7017fd40270", size = 146740 }, + { url = "https://files.pythonhosted.org/packages/69/b3/89640bd676e26ea2315b5aaf80712a6fbbb4338e4caf872d91448502a19b/simplejson-3.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72e8abbc86fcac83629a030888b45fed3a404d54161118be52cb491cd6975d3e", size = 134440 }, + { url = "https://files.pythonhosted.org/packages/61/20/0035a288deaff05397d6cc0145b33f3dd2429b99cdc880de4c5eca41ca72/simplejson-3.19.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8efb03ca77bd7725dfacc9254df00d73e6f43013cf39bd37ef1a8ed0ebb5165", size = 137949 }, + { url = "https://files.pythonhosted.org/packages/5d/de/5b03fafe3003e32d179588953d38183af6c3747e95c7dcc668c4f9eb886a/simplejson-3.19.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:add8850db04b98507a8b62d248a326ecc8561e6d24336d1ca5c605bbfaab4cad", size = 139992 }, + { url = "https://files.pythonhosted.org/packages/d1/ce/e493116ff49fd215f7baa25195b8f684c91e65c153e2a57e04dc3f3a466b/simplejson-3.19.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fc3dc9fb413fc34c396f52f4c87de18d0bd5023804afa8ab5cc224deeb6a9900", size = 140320 }, + { url = "https://files.pythonhosted.org/packages/86/f3/a18b98a7a27548829f672754dd3940fb637a27981399838128d3e560087f/simplejson-3.19.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dfa420bb9225dd33b6efdabde7c6a671b51150b9b1d9c4e5cd74d3b420b3fe1", size = 148625 }, + { url = "https://files.pythonhosted.org/packages/0f/55/d3da33ee3e708133da079b9d537693d7fef281e6f0d27921cc7e5b3ec523/simplejson-3.19.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7b5c472099b39b274dcde27f1113db8d818c9aa3ba8f78cbb8ad04a4c1ac2118", size = 141287 }, + { url = "https://files.pythonhosted.org/packages/17/e8/56184ab4d66bb64a6ff569f069b3796dfd943f9b961268fe0d403526fc17/simplejson-3.19.3-cp310-cp310-win32.whl", hash = "sha256:817abad79241ed4a507b3caf4d3f2be5079f39d35d4c550a061988986bffd2ec", size = 74143 }, + { url = "https://files.pythonhosted.org/packages/be/8f/a0089eff060f10a925f08b0a0f50854321484f1ac54b1895bbf4c9213dfe/simplejson-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:dd5b9b1783e14803e362a558680d88939e830db2466f3fa22df5c9319f8eea94", size = 75643 }, + { url = "https://files.pythonhosted.org/packages/8c/bb/9ee3959e6929d228cf669b3f13f0edd43c5261b6cd69598640748b19ca35/simplejson-3.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e88abff510dcff903a18d11c2a75f9964e768d99c8d147839913886144b2065e", size = 91930 }, + { url = "https://files.pythonhosted.org/packages/ac/ae/a06523928af3a6783e2638cd4f6035c3e32de1c1063d563d9060c8d2f1ad/simplejson-3.19.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:934a50a614fb831614db5dbfba35127ee277624dda4d15895c957d2f5d48610c", size = 74787 }, + { url = "https://files.pythonhosted.org/packages/c3/58/fea732e48a7540035fe46d39e6fd77679f5810311d31da8661ce7a18210a/simplejson-3.19.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:212fce86a22188b0c7f53533b0f693ea9605c1a0f02c84c475a30616f55a744d", size = 74612 }, + { url = "https://files.pythonhosted.org/packages/ab/4d/15718f20cb0e3875b8af9597d6bb3bfbcf1383834b82b6385ee9ac0b72a9/simplejson-3.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d9e8f836688a8fabe6a6b41b334aa550a6823f7b4ac3d3712fc0ad8655be9a8", size = 143550 }, + { url = "https://files.pythonhosted.org/packages/93/44/815a4343774760f7a82459c8f6a4d8268b4b6d23f81e7b922a5e2ca79171/simplejson-3.19.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23228037dc5d41c36666384062904d74409a62f52283d9858fa12f4c22cffad1", size = 153284 }, + { url = "https://files.pythonhosted.org/packages/9d/52/d3202d9bba95444090d1c98e43da3c10907875babf63ed3c134d1b9437e3/simplejson-3.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0791f64fed7d4abad639491f8a6b1ba56d3c604eb94b50f8697359b92d983f36", size = 141518 }, + { url = "https://files.pythonhosted.org/packages/b7/d4/850948bcbcfe0b4a6c69dfde10e245d3a1ea45252f16a1e2308a3b06b1da/simplejson-3.19.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f614581b61a26fbbba232a1391f6cee82bc26f2abbb6a0b44a9bba25c56a1c", size = 144688 }, + { url = "https://files.pythonhosted.org/packages/58/d2/b8dcb0a07d9cd54c47f9fe8733dbb83891d1efe4fc786d9dfc8781cc04f9/simplejson-3.19.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1df0aaf1cb787fdf34484ed4a1f0c545efd8811f6028623290fef1a53694e597", size = 144534 }, + { url = "https://files.pythonhosted.org/packages/a9/95/1e92d99039041f596e0923ec4f9153244acaf3830944dc69a7c11b23ceaa/simplejson-3.19.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:951095be8d4451a7182403354c22ec2de3e513e0cc40408b689af08d02611588", size = 146565 }, + { url = "https://files.pythonhosted.org/packages/21/04/c96aeb3a74031255e4cbcc0ca1b6ebfb5549902f0a065f06d65ce8447c0c/simplejson-3.19.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a954b30810988feeabde843e3263bf187697e0eb5037396276db3612434049b", size = 155014 }, + { url = "https://files.pythonhosted.org/packages/b7/41/e28a28593afc4a75d8999d057bfb7c73a103e35f927e66f4bb92571787ae/simplejson-3.19.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c40df31a75de98db2cdfead6074d4449cd009e79f54c1ebe5e5f1f153c68ad20", size = 148092 }, + { url = "https://files.pythonhosted.org/packages/2b/82/1c81a3af06f937afb6d2e9d74a465c0e0ae6db444d1bf2a436ea26de1965/simplejson-3.19.3-cp311-cp311-win32.whl", hash = "sha256:7e2a098c21ad8924076a12b6c178965d88a0ad75d1de67e1afa0a66878f277a5", size = 73942 }, + { url = "https://files.pythonhosted.org/packages/65/be/d8ab9717f471be3c114f16abd8be21d9a6a0a09b9b49177d93d64d3717d9/simplejson-3.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:c9bedebdc5fdad48af8783022bae307746d54006b783007d1d3c38e10872a2c6", size = 75469 }, + { url = "https://files.pythonhosted.org/packages/20/15/513fea93fafbdd4993eacfcb762965b2ff3d29e618c029e2956174d68c4b/simplejson-3.19.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:66a0399e21c2112acacfebf3d832ebe2884f823b1c7e6d1363f2944f1db31a99", size = 92921 }, + { url = "https://files.pythonhosted.org/packages/a4/4f/998a907ae1a6c104dc0ee48aa248c2478490152808d34d8e07af57f396c3/simplejson-3.19.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6ef9383c5e05f445be60f1735c1816163c874c0b1ede8bb4390aff2ced34f333", size = 75311 }, + { url = "https://files.pythonhosted.org/packages/db/44/acd6122201e927451869d45952b9ab1d3025cdb5e61548d286d08fbccc08/simplejson-3.19.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42e5acf80d4d971238d4df97811286a044d720693092b20a56d5e56b7dcc5d09", size = 74964 }, + { url = "https://files.pythonhosted.org/packages/27/ca/d0a1e8f16e1bbdc0b8c6d88166f45f565ed7285f53928cfef3b6ce78f14d/simplejson-3.19.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0b0efc7279d768db7c74d3d07f0b5c81280d16ae3fb14e9081dc903e8360771", size = 150106 }, + { url = "https://files.pythonhosted.org/packages/63/59/0554b78cf26c98e2b9cae3f44723bd72c2394e2afec1a14eedc6211f7187/simplejson-3.19.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0552eb06e7234da892e1d02365cd2b7b2b1f8233aa5aabdb2981587b7cc92ea0", size = 158347 }, + { url = "https://files.pythonhosted.org/packages/b2/fe/9f30890352e431e8508cc569912d3322147d3e7e4f321e48c0adfcb4c97d/simplejson-3.19.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf6a3b9a7d7191471b464fe38f684df10eb491ec9ea454003edb45a011ab187", size = 148456 }, + { url = "https://files.pythonhosted.org/packages/37/e3/663a09542ee021d4131162f7a164cb2e7f04ef48433a67591738afbf12ea/simplejson-3.19.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7017329ca8d4dca94ad5e59f496e5fc77630aecfc39df381ffc1d37fb6b25832", size = 152190 }, + { url = "https://files.pythonhosted.org/packages/31/20/4e0c4d35e10ff6465003bec304316d822a559a1c38c66ef6892ca199c207/simplejson-3.19.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:67a20641afebf4cfbcff50061f07daad1eace6e7b31d7622b6fa2c40d43900ba", size = 149846 }, + { url = "https://files.pythonhosted.org/packages/08/7a/46e2e072cac3987cbb05946f25167f0ad2fe536748e7405953fd6661a486/simplejson-3.19.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd6a7dabcc4c32daf601bc45e01b79175dde4b52548becea4f9545b0a4428169", size = 151714 }, + { url = "https://files.pythonhosted.org/packages/7f/7d/dbeeac10eb61d5d8858d0bb51121a21050d281dc83af4c557f86da28746c/simplejson-3.19.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:08f9b443a94e72dd02c87098c96886d35790e79e46b24e67accafbf13b73d43b", size = 158777 }, + { url = "https://files.pythonhosted.org/packages/fc/8f/a98bdbb799c6a4a884b5823db31785a96ba895b4b0f4d8ac345d6fe98bbf/simplejson-3.19.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa97278ae6614346b5ca41a45a911f37a3261b57dbe4a00602048652c862c28b", size = 154230 }, + { url = "https://files.pythonhosted.org/packages/b1/db/852eebceb85f969ae40e06babed1a93d3bacb536f187d7a80ff5823a5979/simplejson-3.19.3-cp312-cp312-win32.whl", hash = "sha256:ef28c3b328d29b5e2756903aed888960bc5df39b4c2eab157ae212f70ed5bf74", size = 74002 }, + { url = "https://files.pythonhosted.org/packages/fe/68/9f0e5df0651cb79ef83cba1378765a00ee8038e6201cc82b8e7178a7778e/simplejson-3.19.3-cp312-cp312-win_amd64.whl", hash = "sha256:1e662336db50ad665777e6548b5076329a94a0c3d4a0472971c588b3ef27de3a", size = 75596 }, + { url = "https://files.pythonhosted.org/packages/93/3a/5896821ed543899fcb9c4256c7e71bb110048047349a00f42bc8b8fb379f/simplejson-3.19.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0959e6cb62e3994b5a40e31047ff97ef5c4138875fae31659bead691bed55896", size = 92931 }, + { url = "https://files.pythonhosted.org/packages/39/15/5d33d269440912ee40d856db0c8be2b91aba7a219690ab01f86cb0edd590/simplejson-3.19.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a7bfad839c624e139a4863007233a3f194e7c51551081f9789cba52e4da5167", size = 75318 }, + { url = "https://files.pythonhosted.org/packages/2a/8d/2e7483a2bf7ec53acf7e012bafbda79d7b34f90471dda8e424544a59d484/simplejson-3.19.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afab2f7f2486a866ff04d6d905e9386ca6a231379181a3838abce1f32fbdcc37", size = 74971 }, + { url = "https://files.pythonhosted.org/packages/4d/9d/9bdf34437c8834a7cf7246f85e9d5122e30579f512c10a0c2560e994294f/simplejson-3.19.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00313681015ac498e1736b304446ee6d1c72c5b287cd196996dad84369998f7", size = 150112 }, + { url = "https://files.pythonhosted.org/packages/a7/e2/1f2ae2d89eaf85f6163c82150180aae5eaa18085cfaf892f8a57d4c51cbd/simplejson-3.19.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d936ae682d5b878af9d9eb4d8bb1fdd5e41275c8eb59ceddb0aeed857bb264a2", size = 158354 }, + { url = "https://files.pythonhosted.org/packages/60/83/26f610adf234c8492b3f30501e12f2271e67790f946c6898fe0c58aefe99/simplejson-3.19.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c6657485393f2e9b8177c77a7634f13ebe70d5e6de150aae1677d91516ce6b", size = 148455 }, + { url = "https://files.pythonhosted.org/packages/b5/4b/109af50006af77133653c55b5b91b4bd2d579ff8254ce11216c0b75f911b/simplejson-3.19.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a6a750d3c7461b1c47cfc6bba8d9e57a455e7c5f80057d2a82f738040dd1129", size = 152191 }, + { url = "https://files.pythonhosted.org/packages/75/dc/108872a8825cbd99ae6f4334e0490ff1580367baf12198bcaf988f6820ba/simplejson-3.19.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea7a4a998c87c5674a27089e022110a1a08a7753f21af3baf09efe9915c23c3c", size = 149954 }, + { url = "https://files.pythonhosted.org/packages/eb/be/deec1d947a5d0472276ab4a4d1a9378dc5ee27f3dc9e54d4f62ffbad7a08/simplejson-3.19.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6300680d83a399be2b8f3b0ef7ef90b35d2a29fe6e9c21438097e0938bbc1564", size = 151812 }, + { url = "https://files.pythonhosted.org/packages/e9/58/4ee130702d36b1551ef66e7587eefe56651f3669255bf748cd71691e2434/simplejson-3.19.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ab69f811a660c362651ae395eba8ce84f84c944cea0df5718ea0ba9d1e4e7252", size = 158880 }, + { url = "https://files.pythonhosted.org/packages/0f/e1/59cc6a371b60f89e3498d9f4c8109f6b7359094d453f5fe80b2677b777b0/simplejson-3.19.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:256e09d0f94d9c3d177d9e95fd27a68c875a4baa2046633df387b86b652f5747", size = 154344 }, + { url = "https://files.pythonhosted.org/packages/79/45/1b36044670016f5cb25ebd92497427d2d1711ecb454d00f71eb9a00b77cc/simplejson-3.19.3-cp313-cp313-win32.whl", hash = "sha256:2c78293470313aefa9cfc5e3f75ca0635721fb016fb1121c1c5b0cb8cc74712a", size = 74002 }, + { url = "https://files.pythonhosted.org/packages/e2/58/b06226e6b0612f2b1fa13d5273551da259f894566b1eef32249ddfdcce44/simplejson-3.19.3-cp313-cp313-win_amd64.whl", hash = "sha256:3bbcdc438dc1683b35f7a8dc100960c721f922f9ede8127f63bed7dfded4c64c", size = 75599 }, + { url = "https://files.pythonhosted.org/packages/9a/3d/e7f1caf7fa8c004c30e2c0595a22646a178344a7f53924c11c3d263a8623/simplejson-3.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b5587feda2b65a79da985ae6d116daf6428bf7489992badc29fc96d16cd27b05", size = 93646 }, + { url = "https://files.pythonhosted.org/packages/01/40/ff5cae1b4ff35c7822456ad7d098371d697479d418194064b8aff8142d70/simplejson-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0d2b00ecbcd1a3c5ea1abc8bb99a26508f758c1759fd01c3be482a3655a176f", size = 75544 }, + { url = "https://files.pythonhosted.org/packages/56/a8/dbe799f3620a08337ff5f3be27df7b5ba5beb1ee06acaf75f3cb46f8d650/simplejson-3.19.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:32a3ada8f3ea41db35e6d37b86dade03760f804628ec22e4fe775b703d567426", size = 75593 }, + { url = "https://files.pythonhosted.org/packages/d5/53/6ed299b9201ea914bb6a178a7e65413ed1969981533f50bfbe8a215be98f/simplejson-3.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f455672f4738b0f47183c5896e3606cd65c9ddee3805a4d18e8c96aa3f47c84", size = 138077 }, + { url = "https://files.pythonhosted.org/packages/1c/73/14306559157a6faedb4ecae28ad907b64b5359be5c9ec79233546acb96a4/simplejson-3.19.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b737a5fefedb8333fa50b8db3dcc9b1d18fd6c598f89fa7debff8b46bf4e511", size = 146307 }, + { url = "https://files.pythonhosted.org/packages/5b/1a/7994abb33e53ec972dd5e6dbb337b9070d3ad96017c4cff9d5dc83678ad4/simplejson-3.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb47ee773ce67476a960e2db4a0a906680c54f662521550828c0cc57d0099426", size = 133922 }, + { url = "https://files.pythonhosted.org/packages/08/15/8b4e1a8c7729b37797d0eab1381f517f928bd323d17efa7f4414c3565e1f/simplejson-3.19.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eed8cd98a7b24861da9d3d937f5fbfb6657350c547528a117297fe49e3960667", size = 137367 }, + { url = "https://files.pythonhosted.org/packages/59/9a/f5b786fe611395564d3e84f58f668242a7a2e674b4fac71b4e6b21d6d2b7/simplejson-3.19.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:619756f1dd634b5bdf57d9a3914300526c3b348188a765e45b8b08eabef0c94e", size = 139513 }, + { url = "https://files.pythonhosted.org/packages/4d/87/c310daf5e2f10306de3720f075f8ed74cbe83396879b8c55e832393233a5/simplejson-3.19.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dd7230d061e755d60a4d5445bae854afe33444cdb182f3815cff26ac9fb29a15", size = 139749 }, + { url = "https://files.pythonhosted.org/packages/fd/89/690880e1639b421a919d36fadf1fc364a38c3bc4f208dc11627426cdbe98/simplejson-3.19.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:101a3c8392028cd704a93c7cba8926594e775ca3c91e0bee82144e34190903f1", size = 148103 }, + { url = "https://files.pythonhosted.org/packages/a3/31/ef13eda5b5a0d8d9555b70151ee2956f63b845e1fac4ff904339dfb4dd89/simplejson-3.19.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e557712fc79f251673aeb3fad3501d7d4da3a27eff0857af2e1d1afbbcf6685", size = 140740 }, + { url = "https://files.pythonhosted.org/packages/39/5f/26b0a036592e45a2cb4be2f53d8827257e169bd5c84744a1aac89b0ff56f/simplejson-3.19.3-cp39-cp39-win32.whl", hash = "sha256:0bc5544e3128891bf613b9f71813ee2ec9c11574806f74dd8bb84e5e95bf64a2", size = 74115 }, + { url = "https://files.pythonhosted.org/packages/32/06/a35e2e1d8850aff1cf1320d4887bd5f97921c8964a1e260983d38d5d6c17/simplejson-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:06662392e4913dc8846d6a71a6d5de86db5fba244831abe1dd741d62a4136764", size = 75636 }, + { url = "https://files.pythonhosted.org/packages/0d/e7/f9fafbd4f39793a20cc52e77bbd766f7384312526d402c382928dc7667f6/simplejson-3.19.3-py3-none-any.whl", hash = "sha256:49cc4c7b940d43bd12bf87ec63f28cbc4964fc4e12c031cc8cd01650f43eb94e", size = 57004 }, +] + [[package]] name = "six" version = "1.17.0" @@ -1229,6 +1751,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] +[[package]] +name = "virtualenv" +version = "20.29.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 }, +] + [[package]] name = "watchfiles" version = "1.0.4" @@ -1411,6 +1947,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, ] +[[package]] +name = "zope-event" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824 }, +] + +[[package]] +name = "zope-interface" +version = "7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243 }, + { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759 }, + { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922 }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367 }, + { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488 }, + { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947 }, + { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776 }, + { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296 }, + { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997 }, + { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038 }, + { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806 }, + { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305 }, + { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959 }, + { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357 }, + { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235 }, + { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253 }, + { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702 }, + { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466 }, + { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961 }, + { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356 }, + { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196 }, + { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237 }, + { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696 }, + { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472 }, + { url = "https://files.pythonhosted.org/packages/8c/2c/1f49dc8b4843c4f0848d8e43191aed312bad946a1563d1bf9e46cf2816ee/zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb", size = 208349 }, + { url = "https://files.pythonhosted.org/packages/ed/7d/83ddbfc8424c69579a90fc8edc2b797223da2a8083a94d8dfa0e374c5ed4/zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7", size = 208799 }, + { url = "https://files.pythonhosted.org/packages/36/22/b1abd91854c1be03f5542fe092e6a745096d2eca7704d69432e119100583/zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137", size = 254267 }, + { url = "https://files.pythonhosted.org/packages/2a/dd/fcd313ee216ad0739ae00e6126bc22a0af62a74f76a9ca668d16cd276222/zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519", size = 248614 }, + { url = "https://files.pythonhosted.org/packages/88/d4/4ba1569b856870527cec4bf22b91fe704b81a3c1a451b2ccf234e9e0666f/zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75", size = 253800 }, + { url = "https://files.pythonhosted.org/packages/69/da/c9cfb384c18bd3a26d9fc6a9b5f32ccea49ae09444f097eaa5ca9814aff9/zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d", size = 211980 }, +] + [[package]] name = "zstandard" version = "0.23.0" From 8efa6ec19fc09d95dc2d4515ed4cdc6a81952cb7 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 11:31:28 -0500 Subject: [PATCH 21/29] Remove mock.patch decorator --- test/asynchronous/test_client.py | 16 ++++++++-------- test/test_client.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index b2902371e7..b9d73a540e 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -444,16 +444,16 @@ async def test_metadata(self, simple_client): options = client.options assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE - @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) async def test_container_metadata(self, simple_client): - metadata = copy.deepcopy(_METADATA) - metadata["driver"]["name"] = "PyMongo|async" - metadata["env"] = {} - metadata["env"]["container"] = {"orchestrator": "kubernetes"} + with mock.patch("os.environ", {ENV_VAR_K8S: "1"}): + metadata = copy.deepcopy(_METADATA) + metadata["driver"]["name"] = "PyMongo|async" + metadata["env"] = {} + metadata["env"]["container"] = {"orchestrator": "kubernetes"} - client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") - options = client.options - assert options.pool_options.metadata["env"] == metadata["env"] + client = await simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + options = client.options + assert options.pool_options.metadata["env"] == metadata["env"] async def test_kwargs_codec_options(self, simple_client): class MyFloatType: diff --git a/test/test_client.py b/test/test_client.py index da2fe363ab..60a83d3885 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -426,16 +426,16 @@ def test_metadata(self, simple_client): options = client.options assert len(bson.encode(options.pool_options.metadata)) <= _MAX_METADATA_SIZE - @mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"}) def test_container_metadata(self, simple_client): - metadata = copy.deepcopy(_METADATA) - metadata["driver"]["name"] = "PyMongo" - metadata["env"] = {} - metadata["env"]["container"] = {"orchestrator": "kubernetes"} + with mock.patch("os.environ", {ENV_VAR_K8S: "1"}): + metadata = copy.deepcopy(_METADATA) + metadata["driver"]["name"] = "PyMongo" + metadata["env"] = {} + metadata["env"]["container"] = {"orchestrator": "kubernetes"} - client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") - options = client.options - assert options.pool_options.metadata["env"] == metadata["env"] + client = simple_client("mongodb://foo:27017/?appname=foobar&connect=false") + options = client.options + assert options.pool_options.metadata["env"] == metadata["env"] def test_kwargs_codec_options(self, simple_client): class MyFloatType: From 26b11782182e53b8aa6651fdb26fc93374d1191d Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 11:41:07 -0500 Subject: [PATCH 22/29] test_iteration regex --- test/asynchronous/test_client.py | 9 +++------ test/test_client.py | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index b9d73a540e..a04a98f3e3 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -243,12 +243,9 @@ async def test_getattr(self, async_client): assert "has no attribute '_does_not_exist'" in str(context.value) async def test_iteration(self, async_client): - if _IS_SYNC or sys.version_info < (3, 10): - msg = "'AsyncMongoClient' object is not iterable" - else: - msg = "'AsyncMongoClient' object is not an async iterator" + msg = "'AsyncMongoClient' object is not iterable" - with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + with pytest.raises(TypeError, match=msg): for _ in async_client: break @@ -261,7 +258,7 @@ async def test_iteration(self, async_client): _ = await anext(async_client) # 'next()' method fails - with pytest.raises(TypeError, match="'AsyncMongoClient' object is not iterable"): + with pytest.raises(TypeError, match=msg): _ = await async_client.anext() # Do not implement typing.Iterable diff --git a/test/test_client.py b/test/test_client.py index 60a83d3885..3457d449d7 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -240,12 +240,9 @@ def test_getattr(self, client): assert "has no attribute '_does_not_exist'" in str(context.value) def test_iteration(self, client): - if _IS_SYNC or sys.version_info < (3, 10): - msg = "'MongoClient' object is not iterable" - else: - msg = "'MongoClient' object is not an async iterator" + msg = "'MongoClient' object is not iterable" - with pytest.raises(TypeError, match="'MongoClient' object is not iterable"): + with pytest.raises(TypeError, match=msg): for _ in client: break @@ -258,7 +255,7 @@ def test_iteration(self, client): _ = next(client) # 'next()' method fails - with pytest.raises(TypeError, match="'MongoClient' object is not iterable"): + with pytest.raises(TypeError, match=msg): _ = client.next() # Do not implement typing.Iterable From 969423906693c9d69d5ca3bc96a61599e8510155 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 14:12:45 -0500 Subject: [PATCH 23/29] run-tests.sh hacking for compatibility --- .evergreen/run-tests.sh | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 46f6b1cca8..c1dc8c4878 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -268,20 +268,32 @@ if [ -z "$GREEN_FRAMEWORK" ]; then PYTEST_ARGS="-v --capture=tee-sys --durations=5 $TEST_ARGS" if [ -n "$TEST_SUITES" ]; then # Workaround until unittest -> pytest conversion is complete + if [[ "$TEST_SUITES" == *"default_async"* ]]; then + # shellcheck disable=SC2206 + ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) + else + # shellcheck disable=SC2206 + ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) + fi # shellcheck disable=SC2206 - ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) - PYTEST_ARGS="-m $TEST_SUITES $PYTEST_ARGS" + PYTEST_ARGS=("-m $TEST_SUITES and not asyncio" $PYTEST_ARGS) fi # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest $PYTEST_ARGS + uv run ${UV_ARGS[*]} pytest "${PYTEST_ARGS[@]}" # Workaround until unittest -> pytest conversion is complete - if [ -z "$TEST_SUITES" ]; then - # shellcheck disable=SC2206 - ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) + if [ -n "$TEST_SUITES" ]; then + set +o errexit + # shellcheck disable=SC2048 + uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" "--collect-only" + collected=$? + set -o errexit + # If we collected at least one async test, run all collected tests + if [ $collected -ne 5 ]; then + # shellcheck disable=SC2048 + uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" + fi fi - # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" else # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS From 9c3939bf246b5975aa9d608de22c14a5fae2b7e4 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 15:00:22 -0500 Subject: [PATCH 24/29] run-tests.sh should use arrays for arguments --- .evergreen/run-tests.sh | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index c1dc8c4878..8ec396e0dd 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -265,18 +265,15 @@ PIP_QUIET=0 uv run ${UV_ARGS[*]} --with pip pip list if [ -z "$GREEN_FRAMEWORK" ]; then # Use --capture=tee-sys so pytest prints test output inline: # https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html - PYTEST_ARGS="-v --capture=tee-sys --durations=5 $TEST_ARGS" + PYTEST_ARGS=("-v" "--capture=tee-sys" "--durations=5" "$TEST_ARGS") if [ -n "$TEST_SUITES" ]; then # Workaround until unittest -> pytest conversion is complete if [[ "$TEST_SUITES" == *"default_async"* ]]; then - # shellcheck disable=SC2206 - ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) + ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}") else - # shellcheck disable=SC2206 - ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" $PYTEST_ARGS) + ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}") fi - # shellcheck disable=SC2206 - PYTEST_ARGS=("-m $TEST_SUITES and not asyncio" $PYTEST_ARGS) + PYTEST_ARGS=("-m $TEST_SUITES and not asyncio" "${PYTEST_ARGS[@]}") fi # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest "${PYTEST_ARGS[@]}" From 1a643c3a1be92076bba7d29f8a563c6702c73ca9 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 24 Jan 2025 15:37:23 -0500 Subject: [PATCH 25/29] TEST_ARGS should also use array --- .evergreen/run-tests.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 8ec396e0dd..15026953e2 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -31,7 +31,7 @@ set -o xtrace AUTH=${AUTH:-noauth} SSL=${SSL:-nossl} TEST_SUITES=${TEST_SUITES:-} -TEST_ARGS="${*:1}" +TEST_ARGS=("${*:1}") export PIP_QUIET=1 # Quiet by default export PIP_PREFER_BINARY=1 # Prefer binary dists by default @@ -206,6 +206,7 @@ if [ -n "$TEST_INDEX_MANAGEMENT" ]; then TEST_SUITES="index_management" fi +# shellcheck disable=SC2128 if [ -n "$TEST_DATA_LAKE" ] && [ -z "$TEST_ARGS" ]; then TEST_SUITES="data_lake" fi @@ -235,7 +236,7 @@ if [ -n "$PERF_TEST" ]; then TEST_SUITES="perf" # PYTHON-4769 Run perf_test.py directly otherwise pytest's test collection negatively # affects the benchmark results. - TEST_ARGS="test/performance/perf_test.py $TEST_ARGS" + TEST_ARGS+=("test/performance/perf_test.py") fi echo "Running $AUTH tests over $SSL with python $(uv python find)" @@ -251,7 +252,7 @@ if [ -n "$COVERAGE" ] && [ "$PYTHON_IMPL" = "CPython" ]; then # Keep in sync with combine-coverage.sh. # coverage >=5 is needed for relative_files=true. UV_ARGS+=("--group coverage") - TEST_ARGS="$TEST_ARGS --cov" + TEST_ARGS+=("--cov") fi if [ -n "$GREEN_FRAMEWORK" ]; then @@ -265,7 +266,7 @@ PIP_QUIET=0 uv run ${UV_ARGS[*]} --with pip pip list if [ -z "$GREEN_FRAMEWORK" ]; then # Use --capture=tee-sys so pytest prints test output inline: # https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html - PYTEST_ARGS=("-v" "--capture=tee-sys" "--durations=5" "$TEST_ARGS") + PYTEST_ARGS=("-v" "--capture=tee-sys" "--durations=5" "${TEST_ARGS[@]}") if [ -n "$TEST_SUITES" ]; then # Workaround until unittest -> pytest conversion is complete if [[ "$TEST_SUITES" == *"default_async"* ]]; then @@ -293,7 +294,7 @@ if [ -z "$GREEN_FRAMEWORK" ]; then fi else # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS + uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v "${TEST_ARGS[@]}" fi # Handle perf test post actions. From 90b469363d50d9db696d437b987de166d4eace4b Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 27 Jan 2025 09:43:50 -0500 Subject: [PATCH 26/29] Undo unintended test/__init__.py changes --- test/__init__.py | 17 +++++++++++++++-- test/asynchronous/__init__.py | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 8cd73009df..3661c272e0 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -22,6 +22,7 @@ import socket import subprocess import sys +import threading import time import unittest import warnings @@ -113,7 +114,7 @@ def __init__(self): self.default_client_options: Dict = {} self.sessions_enabled = False self.client = None # type: ignore - self.conn_lock = _create_lock() + self.conn_lock = threading.Lock() self.is_data_lake = False self.load_balancer = TEST_LOADBALANCER self.serverless = TEST_SERVERLESS @@ -697,7 +698,7 @@ def is_topology_type(self, topologies): if "sharded" in topologies and self.is_mongos: return True if "sharded-replicaset" in topologies and self.is_mongos: - shards = self.client.config.shards.find().to_list() + shards = client_context.client.config.shards.find().to_list() for shard in shards: # For a 3-member RS-backed sharded cluster, shard['host'] # will be 'replicaName/ip1:port1,ip2:port2,ip3:port3' @@ -871,6 +872,16 @@ def max_message_size_bytes(self): client_context = ClientContext() +def reset_client_context(): + if _IS_SYNC: + # sync tests don't need to reset a client context + return + elif client_context.client is not None: + client_context.client.close() + client_context.client = None + client_context._init_client() + + class PyMongoTestCasePyTest: @contextmanager def fail_point(self, client, command_args): @@ -1145,6 +1156,8 @@ class IntegrationTest(PyMongoTestCase): @client_context.require_connection def setUp(self) -> None: + if not _IS_SYNC: + reset_client_context() if client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") if client_context.serverless and not getattr(self, "RUN_ON_SERVERLESS", False): diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 13cfbe1d4c..114b21fe45 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -22,6 +22,7 @@ import socket import subprocess import sys +import threading import time import unittest import warnings @@ -113,7 +114,7 @@ def __init__(self): self.default_client_options: Dict = {} self.sessions_enabled = False self.client = None # type: ignore - self.conn_lock = _async_create_lock() + self.conn_lock = threading.Lock() self.is_data_lake = False self.load_balancer = TEST_LOADBALANCER self.serverless = TEST_SERVERLESS @@ -334,7 +335,7 @@ async def _init_client(self): await mongos_client.close() async def init(self): - async with self.conn_lock: + with self.conn_lock: if not self.client and not self.connection_attempts: await self._init_client() @@ -699,7 +700,7 @@ async def is_topology_type(self, topologies): if "sharded" in topologies and self.is_mongos: return True if "sharded-replicaset" in topologies and self.is_mongos: - shards = await self.client.config.shards.find().to_list() + shards = await async_client_context.client.config.shards.find().to_list() for shard in shards: # For a 3-member RS-backed sharded cluster, shard['host'] # will be 'replicaName/ip1:port1,ip2:port2,ip3:port3' @@ -873,6 +874,16 @@ async def max_message_size_bytes(self): async_client_context = AsyncClientContext() +async def reset_client_context(): + if _IS_SYNC: + # sync tests don't need to reset a client context + return + elif async_client_context.client is not None: + await async_client_context.client.close() + async_client_context.client = None + await async_client_context._init_client() + + class AsyncPyMongoTestCasePyTest: @asynccontextmanager async def fail_point(self, client, command_args): @@ -1165,6 +1176,8 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase): @async_client_context.require_connection async def asyncSetUp(self) -> None: + if not _IS_SYNC: + await reset_client_context() if async_client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") if async_client_context.serverless and not getattr(self, "RUN_ON_SERVERLESS", False): From 0791a8fdf185f28c47b8511e89b13f09cf70f48b Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 27 Jan 2025 11:12:52 -0500 Subject: [PATCH 27/29] More run-tests.sh fixes --- .evergreen/run-tests.sh | 20 ++++++++++---------- pyproject.toml | 2 +- test/__init__.py | 1 - test/asynchronous/__init__.py | 1 - 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 15026953e2..d6cc7a26bd 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -275,22 +275,22 @@ if [ -z "$GREEN_FRAMEWORK" ]; then ASYNC_PYTEST_ARGS=("-m asyncio and $TEST_SUITES" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}") fi PYTEST_ARGS=("-m $TEST_SUITES and not asyncio" "${PYTEST_ARGS[@]}") + else + ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}") fi # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest "${PYTEST_ARGS[@]}" # Workaround until unittest -> pytest conversion is complete - if [ -n "$TEST_SUITES" ]; then - set +o errexit + set +o errexit + # shellcheck disable=SC2048 + uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" "--collect-only" + collected=$? + set -o errexit + # If we collected at least one async test, run all collected tests + if [ $collected -ne 5 ]; then # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" "--collect-only" - collected=$? - set -o errexit - # If we collected at least one async test, run all collected tests - if [ $collected -ne 5 ]; then - # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" - fi + uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" fi else # shellcheck disable=SC2048 diff --git a/pyproject.toml b/pyproject.toml index 75e130594e..e3540f1bc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ zstd = ["requirements/zstd.txt"] [tool.pytest.ini_options] minversion = "7" -addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async"] +addopts = ["-ra", "--strict-config", "--strict-markers", "--junitxml=xunit-results/TEST-results.xml", "-m default or default_async and not asyncio"] testpaths = ["test"] log_cli_level = "INFO" faulthandler_timeout = 1500 diff --git a/test/__init__.py b/test/__init__.py index 3661c272e0..7f8b2b5cc8 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -50,7 +50,6 @@ sanitize_reply, ) -from pymongo.lock import _create_lock from pymongo.uri_parser import parse_uri try: diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 114b21fe45..631100d766 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -50,7 +50,6 @@ sanitize_reply, ) -from pymongo.lock import _async_create_lock from pymongo.uri_parser import parse_uri try: From 3f1a86c6d0f5a99a83b664e684ebc1756e87bdb8 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 27 Jan 2025 13:02:56 -0500 Subject: [PATCH 28/29] Errexit hacking --- .evergreen/run-tests.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index d6cc7a26bd..1564cd0206 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -278,11 +278,12 @@ if [ -z "$GREEN_FRAMEWORK" ]; then else ASYNC_PYTEST_ARGS=("-m asyncio" "--junitxml=xunit-results/TEST-asyncresults.xml" "${PYTEST_ARGS[@]}") fi + # Workaround until unittest -> pytest conversion is complete + set +o errexit # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest "${PYTEST_ARGS[@]}" + exit_code=$? - # Workaround until unittest -> pytest conversion is complete - set +o errexit # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" "--collect-only" collected=$? @@ -292,6 +293,9 @@ if [ -z "$GREEN_FRAMEWORK" ]; then # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" fi + if [ $exit_code -ne 0 ]; then + exit $exit_code + fi else # shellcheck disable=SC2048 uv run ${UV_ARGS[*]} green_framework_test.py $GREEN_FRAMEWORK -v "${TEST_ARGS[@]}" From 077a1cb9bd67eecf09bacdf6600e51055161194a Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 28 Jan 2025 10:19:01 -0500 Subject: [PATCH 29/29] Address review --- .evergreen/run-tests.sh | 10 ++++------ test/asynchronous/pymongo_mocks.py | 2 +- test/asynchronous/test_client.py | 11 ++++++++++- test/pymongo_mocks.py | 2 +- test/test_client.py | 11 ++++++++++- test/utils.py | 4 ++-- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 1564cd0206..16dad5eac4 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -285,13 +285,11 @@ if [ -z "$GREEN_FRAMEWORK" ]; then exit_code=$? # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" "--collect-only" - collected=$? + uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" + async_exit_code=$? set -o errexit - # If we collected at least one async test, run all collected tests - if [ $collected -ne 5 ]; then - # shellcheck disable=SC2048 - uv run ${UV_ARGS[*]} pytest "${ASYNC_PYTEST_ARGS[@]}" + if [ $async_exit_code -ne 5 ] && [ $async_exit_code -ne 0 ]; then + exit $async_exit_code fi if [ $exit_code -ne 0 ]; then exit $exit_code diff --git a/test/asynchronous/pymongo_mocks.py b/test/asynchronous/pymongo_mocks.py index 4703d3a194..6f5bedeff6 100644 --- a/test/asynchronous/pymongo_mocks.py +++ b/test/asynchronous/pymongo_mocks.py @@ -166,7 +166,7 @@ async def get_async_mock_client( standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs ) - if "connect" not in kwargs or "connect" in kwargs and kwargs["connect"]: + if kwargs.get("connect", True): await c.aconnect() return c diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index a04a98f3e3..1249db7d20 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -2393,7 +2393,16 @@ class TestClientLazyConnect: @pytest.fixture def _get_client(self, async_rs_or_single_client): - return async_rs_or_single_client(connect=False) + clients = [] + + def _make_client(): + client = async_rs_or_single_client(connect=False) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() def test_insert_one(self, _get_client, async_client_context_fixture): def reset(collection): diff --git a/test/pymongo_mocks.py b/test/pymongo_mocks.py index 243d9eb98d..d352aae070 100644 --- a/test/pymongo_mocks.py +++ b/test/pymongo_mocks.py @@ -165,7 +165,7 @@ def get_mock_client( standalones, members, mongoses, hello_hosts, arbiters, down_hosts, *args, **kwargs ) - if "connect" not in kwargs or "connect" in kwargs and kwargs["connect"]: + if kwargs.get("connect", True): c._connect() return c diff --git a/test/test_client.py b/test/test_client.py index 3457d449d7..35f2fe67b4 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -2297,7 +2297,16 @@ class TestClientLazyConnect: @pytest.fixture def _get_client(self, rs_or_single_client): - return rs_or_single_client(connect=False) + clients = [] + + def _make_client(): + client = rs_or_single_client(connect=False) + clients.append(client) + return client + + yield _make_client + for client in clients: + client.close() def test_insert_one(self, _get_client, client_context_fixture): def reset(collection): diff --git a/test/utils.py b/test/utils.py index bd8de4ecee..bcbb1f9759 100644 --- a/test/utils.py +++ b/test/utils.py @@ -811,7 +811,7 @@ def frequent_thread_switches(): sys.setswitchinterval(interval) -def lazy_client_trial(reset, target, test, client, client_context): +def lazy_client_trial(reset, target, test, get_client, client_context): """Test concurrent operations on a lazily-connecting client. `reset` takes a collection and resets it for the next trial. @@ -827,7 +827,7 @@ def lazy_client_trial(reset, target, test, client, client_context): with frequent_thread_switches(): for _i in range(NTRIALS): reset(collection) - lazy_client = client + lazy_client = get_client() lazy_collection = lazy_client.pymongo_test.test run_threads(lazy_collection, target) test(lazy_collection)