diff --git a/.evergreen/generated_configs/variants.yml b/.evergreen/generated_configs/variants.yml index 4fb5a36250..a05cc61f05 100644 --- a/.evergreen/generated_configs/variants.yml +++ b/.evergreen/generated_configs/variants.yml @@ -535,6 +535,8 @@ buildvariants: display_name: "* MongoDB v4.2" run_on: - rhel87-small + expansions: + VERSION: "4.2" tags: [coverage_tag] - name: mongodb-v4.4 tasks: @@ -542,6 +544,8 @@ buildvariants: display_name: "* MongoDB v4.4" run_on: - rhel87-small + expansions: + VERSION: "4.4" tags: [coverage_tag] - name: mongodb-v5.0 tasks: @@ -549,6 +553,8 @@ buildvariants: display_name: "* MongoDB v5.0" run_on: - rhel87-small + expansions: + VERSION: "5.0" tags: [coverage_tag] - name: mongodb-v6.0 tasks: @@ -556,6 +562,8 @@ buildvariants: display_name: "* MongoDB v6.0" run_on: - rhel87-small + expansions: + VERSION: "6.0" tags: [coverage_tag] - name: mongodb-v7.0 tasks: @@ -563,6 +571,8 @@ buildvariants: display_name: "* MongoDB v7.0" run_on: - rhel87-small + expansions: + VERSION: "7.0" tags: [coverage_tag] - name: mongodb-v8.0 tasks: @@ -570,6 +580,8 @@ buildvariants: display_name: "* MongoDB v8.0" run_on: - rhel87-small + expansions: + VERSION: "8.0" tags: [coverage_tag] - name: mongodb-rapid tasks: @@ -577,6 +589,8 @@ buildvariants: display_name: "* MongoDB rapid" run_on: - rhel87-small + expansions: + VERSION: rapid tags: [coverage_tag] - name: mongodb-latest tasks: @@ -584,6 +598,8 @@ buildvariants: display_name: "* MongoDB latest" run_on: - rhel87-small + expansions: + VERSION: latest tags: [coverage_tag] # Stable api tests diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh index a60b112bcb..b34013a6ac 100755 --- a/.evergreen/run-mongodb-oidc-test.sh +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -8,7 +8,9 @@ if [ ${OIDC_ENV} == "k8s" ]; then SUB_TEST_NAME=$K8S_VARIANT-remote else SUB_TEST_NAME=$OIDC_ENV-remote + sudo apt-get install -y python3-dev build-essential fi + bash ./.evergreen/just.sh setup-tests auth_oidc $SUB_TEST_NAME bash ./.evergreen/just.sh run-tests "${@:1}" diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index 2d160152f7..3a386be4f7 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -74,7 +74,11 @@ def create_server_version_variants() -> list[BuildVariant]: for version in ALL_VERSIONS: display_name = get_variant_name("* MongoDB", version=version) variant = create_variant( - [".server-version"], display_name, host=DEFAULT_HOST, tags=["coverage_tag"] + [".server-version"], + display_name, + version=version, + host=DEFAULT_HOST, + tags=["coverage_tag"], ) variants.append(variant) return variants diff --git a/.evergreen/scripts/install-dependencies.sh b/.evergreen/scripts/install-dependencies.sh index 5425d10c8c..23d865d0d8 100755 --- a/.evergreen/scripts/install-dependencies.sh +++ b/.evergreen/scripts/install-dependencies.sh @@ -48,9 +48,12 @@ if ! command -v just &>/dev/null; then _TARGET="--target x86_64-pc-windows-msvc" fi _BIN_DIR=$PYMONGO_BIN_DIR + mkdir -p ${_BIN_DIR} echo "Installing just..." mkdir -p "$_BIN_DIR" 2>/dev/null || true curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- $_TARGET --to "$_BIN_DIR" || { + # Remove just file if it exists (can be created if there was an install error). + rm -f ${_BIN_DIR}/just _pip_install rust-just just } echo "Installing just... done." @@ -59,6 +62,7 @@ fi # Ensure uv is installed. if ! command -v uv &>/dev/null; then _BIN_DIR=$PYMONGO_BIN_DIR + mkdir -p ${_BIN_DIR} echo "Installing uv..." # On most systems we can install directly. curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$_BIN_DIR" INSTALLER_NO_MODIFY_PATH=1 sh || { diff --git a/.evergreen/scripts/kms_tester.py b/.evergreen/scripts/kms_tester.py index 40fd65919d..3579e77619 100644 --- a/.evergreen/scripts/kms_tester.py +++ b/.evergreen/scripts/kms_tester.py @@ -30,6 +30,9 @@ def _setup_azure_vm(base_env: dict[str, str]) -> None: env["AZUREKMS_CMD"] = "tar xf mongo-python-driver.tgz" run_command(f"{azure_dir}/run-command.sh", env=env) + env["AZUREKMS_CMD"] = "sudo apt-get install -y python3-dev build-essential" + run_command(f"{azure_dir}/run-command.sh", env=env) + env["AZUREKMS_CMD"] = "bash .evergreen/just.sh setup-tests kms azure-remote" run_command(f"{azure_dir}/run-command.sh", env=env) LOGGER.info("Setting up Azure VM... done.") @@ -47,6 +50,9 @@ def _setup_gcp_vm(base_env: dict[str, str]) -> None: env["GCPKMS_CMD"] = "tar xf mongo-python-driver.tgz" run_command(f"{gcp_dir}/run-command.sh", env=env) + env["GCPKMS_CMD"] = "sudo apt-get install -y python3-dev build-essential" + run_command(f"{gcp_dir}/run-command.sh", env=env) + env["GCPKMS_CMD"] = "bash ./.evergreen/just.sh setup-tests kms gcp-remote" run_command(f"{gcp_dir}/run-command.sh", env=env) LOGGER.info("Setting up GCP VM...") diff --git a/.evergreen/scripts/oidc_tester.py b/.evergreen/scripts/oidc_tester.py index fd702cf1d1..d6f127bbd6 100644 --- a/.evergreen/scripts/oidc_tester.py +++ b/.evergreen/scripts/oidc_tester.py @@ -2,7 +2,14 @@ import os -from utils import DRIVERS_TOOLS, TMP_DRIVER_FILE, create_archive, read_env, run_command, write_env +from utils import ( + DRIVERS_TOOLS, + TMP_DRIVER_FILE, + create_archive, + read_env, + run_command, + write_env, +) K8S_NAMES = ["aks", "gke", "eks"] K8S_REMOTE_NAMES = [f"{n}-remote" for n in K8S_NAMES] diff --git a/.evergreen/spec-patch/PYTHON-5493.patch b/.evergreen/spec-patch/PYTHON-5493.patch new file mode 100644 index 0000000000..cf1afbb271 --- /dev/null +++ b/.evergreen/spec-patch/PYTHON-5493.patch @@ -0,0 +1,99 @@ +diff --git a/test/connection_logging/connection-logging.json b/test/connection_logging/connection-logging.json +index d40cfbb7e..5799e834d 100644 +--- a/test/connection_logging/connection-logging.json ++++ b/test/connection_logging/connection-logging.json +@@ -272,7 +272,13 @@ + "level": "debug", + "component": "connection", + "data": { +- "message": "Connection pool closed", ++ "message": "Connection closed", ++ "driverConnectionId": { ++ "$$type": [ ++ "int", ++ "long" ++ ] ++ }, + "serverHost": { + "$$type": "string" + }, +@@ -281,20 +287,15 @@ + "int", + "long" + ] +- } ++ }, ++ "reason": "Connection pool was closed" + } + }, + { + "level": "debug", + "component": "connection", + "data": { +- "message": "Connection closed", +- "driverConnectionId": { +- "$$type": [ +- "int", +- "long" +- ] +- }, ++ "message": "Connection pool closed", + "serverHost": { + "$$type": "string" + }, +@@ -303,8 +304,7 @@ + "int", + "long" + ] +- }, +- "reason": "Connection pool was closed" ++ } + } + } + ] +@@ -446,22 +446,6 @@ + } + } + }, +- { +- "level": "debug", +- "component": "connection", +- "data": { +- "message": "Connection pool cleared", +- "serverHost": { +- "$$type": "string" +- }, +- "serverPort": { +- "$$type": [ +- "int", +- "long" +- ] +- } +- } +- }, + { + "level": "debug", + "component": "connection", +@@ -514,6 +498,22 @@ + ] + } + } ++ }, ++ { ++ "level": "debug", ++ "component": "connection", ++ "data": { ++ "message": "Connection pool cleared", ++ "serverHost": { ++ "$$type": "string" ++ }, ++ "serverPort": { ++ "$$type": [ ++ "int", ++ "long" ++ ] ++ } ++ } + } + ] + } diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index 9cce310d91..cbb2322fe8 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -16,7 +16,7 @@ env: # Changes per repo PRODUCT_NAME: PyMongo # Changes per branch - EVERGREEN_PROJECT: mongo-python-driver + EVERGREEN_PROJECT: mongo-python-driver-release # Constant # inputs will be empty on a scheduled run. so, we only set dry_run # to 'false' when the input is set to 'false'. diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index b7b8fb5062..11255f9e49 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -22,13 +22,13 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - name: Install just - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - name: Install uv uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 with: enable-cache: true python-version: "3.9" + - name: Install just + run: uv tool install rust-just - name: Install Python dependencies run: | just install @@ -50,33 +50,31 @@ jobs: cppcheck pymongo build: - # supercharge/mongodb-github-action requires containers so we don't test other platforms runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + # Tests currently only pass on ubuntu on GitHub Actions. os: [ubuntu-latest] - python-version: ["3.9", "pypy-3.10", "3.13", "3.13t"] + python-version: ["3.9", "pypy-3.10", "3.13t"] + mongodb-version: ["8.0"] + name: CPython ${{ matrix.python-version }}-${{ matrix.os }} steps: - uses: actions/checkout@v4 with: persist-credentials: false - - name: Install just - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - name: Install uv uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 with: enable-cache: true python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: just install - - name: Start MongoDB - uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master with: - mongodb-version: 6.0 + version: "${{ matrix.mongodb-version }}" - name: Run tests - run: just test + run: uv run --extra test pytest -v doctest: runs-on: ubuntu-latest @@ -85,17 +83,17 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - name: Install just - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - name: Install uv uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 with: enable-cache: true python-version: "3.9" - - name: Start MongoDB - uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 + - name: Install just + run: uv tool install rust-just + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master with: - mongodb-version: '8.0.0-rc4' + version: "8.0" - name: Install dependencies run: just install - name: Run tests @@ -116,7 +114,7 @@ jobs: enable-cache: true python-version: "3.9" - name: Install just - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 + run: uv tool install rust-just - name: Install dependencies run: just install - name: Build docs @@ -135,7 +133,7 @@ jobs: enable-cache: true python-version: "3.9" - name: Install just - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 + run: uv tool install rust-just - name: Install dependencies run: just install - name: Build docs @@ -157,7 +155,7 @@ jobs: enable-cache: true python-version: "${{matrix.python}}" - name: Install just - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 + run: uv tool install rust-just - name: Install dependencies run: | just install @@ -210,8 +208,8 @@ jobs: cache-dependency-path: 'sdist/test/pyproject.toml' # Test sdist on lowest supported Python python-version: '3.9' - - name: Start MongoDB - uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master - name: Run connect test from sdist shell: bash run: | @@ -234,10 +232,10 @@ jobs: uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 with: python-version: '3.9' - - name: Start MongoDB - uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master with: - mongodb-version: 6.0 + version: "8.0" # Async and our test_dns do not support dnspython 1.X, so we don't run async or dns tests here - name: Run tests shell: bash @@ -260,10 +258,10 @@ jobs: uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 with: python-version: '3.9' - - name: Start MongoDB - uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master with: - mongodb-version: 6.0 + version: "8.0" # The lifetime kwarg we use in srv resolution was added to the async resolver API in dnspython 2.1.0 - name: Run tests shell: bash diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000000..10fd4cdfcf --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +rules: + unpinned-uses: + config: + policies: + actions/*: ref-pin + mongodb-labs/drivers-github-tools/*: ref-pin + mongodb-labs/drivers-evergreen-tools: ref-pin diff --git a/doc/changelog.rst b/doc/changelog.rst index d88b114fc6..c363eddda5 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,25 +1,61 @@ Changelog ========= +Changes in Version 4.14.1 (2025/08/19) +-------------------------------------- + +Version 4.14.1 is a bug fix release. + + - Fixed a bug in ``MongoClient.append_metadata()`` and ``AsyncMongoClient.append_metadata()`` + that allowed duplicate ``DriverInfo.name`` to be appended to the metadata. + +Issues Resolved +............... + +See the `PyMongo 4.14.1 release notes in JIRA`_ for the list of resolved issues +in this release. + +.. _PyMongo 4.14.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=45256 -Changes in Version 4.14.0 (XXXX/XX/XX) +Changes in Version 4.14.0 (2025/08/06) -------------------------------------- + +.. warning:: PyMongo 4.14 drops support for MongoDB 4.0. PyMongo now supports + MongoDB 4.2+. + PyMongo 4.14 brings a number of changes including: -- Added preliminary support for Python 3.14 and 3.14 with free-threading. We do not yet support the following with Python 3.14: - - Subinterpreters (``concurrent.interpreters``) - - Free-threading with Encryption - - mod_wsgi +- Dropped support for MongoDB 4.0. +- Added preliminary support for Python 3.14 and 3.14 with free-threading. We do + not yet support the following with Python 3.14: + + - Subinterpreters (``concurrent.interpreters``) + - Free-threading with Encryption + - mod_wsgi + - Removed experimental support for free-threading support in Python 3.13. -- Added :attr:`bson.codec_options.TypeRegistry.codecs` and :attr:`bson.codec_options.TypeRegistry.fallback_encoder` properties - to allow users to directly access the type codecs and fallback encoder for a given :class:`bson.codec_options.TypeRegistry`. -- Added :meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.append_metadata` and - :meth:`pymongo.mongo_client.MongoClient.append_metadata` to allow instantiated MongoClients to send client metadata - on-demand +- Added :attr:`bson.codec_options.TypeRegistry.codecs` and + :attr:`bson.codec_options.TypeRegistry.fallback_encoder` properties + to allow users to directly access the type codecs and fallback encoder for a + given :class:`bson.codec_options.TypeRegistry`. +- Added + :meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.append_metadata` and + :meth:`pymongo.mongo_client.MongoClient.append_metadata` to allow instantiated + MongoClients to send client metadata on-demand - Improved performance of selecting a server with the Primary selector. +- Introduces a minor breaking change. When encoding + :class:`bson.binary.BinaryVector`, a ``ValueError`` will be raised if the + 'padding' metadata field is < 0 or > 7, or non-zero for any type other than + PACKED_BIT. +- Changed :meth:`~pymongo.uri_parser.parse_uri`'s ``options`` return value to be + type ``dict`` instead of ``_CaseInsensitiveDictionary``. + +Issues Resolved +............... + +See the `PyMongo 4.14 release notes in JIRA`_ for the list of resolved issues +in this release. -- Introduces a minor breaking change. When encoding :class:`bson.binary.BinaryVector`, a ``ValueError`` will be raised - if the 'padding' metadata field is < 0 or > 7, or non-zero for any type other than PACKED_BIT. -- Changed :meth:`~pymongo.uri_parser.parse_uri`'s ``options`` parameter to be type ``dict`` instead of ``_CaseInsensitiveDictionary``. +.. _PyMongo 4.14 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43041 Changes in Version 4.13.2 (2025/06/17) -------------------------------------- diff --git a/pymongo/_version.py b/pymongo/_version.py index 9e7924773b..9297aef16f 100644 --- a/pymongo/_version.py +++ b/pymongo/_version.py @@ -18,7 +18,7 @@ import re from typing import List, Tuple, Union -__version__ = "4.14.0.dev0" +__version__ = "4.14.2.dev0" def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]: diff --git a/pymongo/asynchronous/encryption.py b/pymongo/asynchronous/encryption.py index 9b0757b1a5..5d91ecaa78 100644 --- a/pymongo/asynchronous/encryption.py +++ b/pymongo/asynchronous/encryption.py @@ -75,12 +75,12 @@ NetworkTimeout, ServerSelectionTimeoutError, ) +from pymongo.helpers_shared import _get_timeout_details from pymongo.network_layer import async_socket_sendall from pymongo.operations import UpdateOne from pymongo.pool_options import PoolOptions from pymongo.pool_shared import ( _async_configured_socket, - _get_timeout_details, _raise_connection_failure, ) from pymongo.read_concern import ReadConcern diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index 9a39883fc2..833d42c526 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -58,6 +58,7 @@ WaitQueueTimeoutError, ) from pymongo.hello import Hello, HelloCompat +from pymongo.helpers_shared import _get_timeout_details, format_timeout_details from pymongo.lock import ( _async_cond_wait, _async_create_condition, @@ -79,9 +80,7 @@ SSLErrors, _CancellationContext, _configured_protocol_interface, - _get_timeout_details, _raise_connection_failure, - format_timeout_details, ) from pymongo.read_preferences import ReadPreference from pymongo.server_api import _add_to_command diff --git a/pymongo/asynchronous/server.py b/pymongo/asynchronous/server.py index 0e0d53b96f..5c7ea50fc6 100644 --- a/pymongo/asynchronous/server.py +++ b/pymongo/asynchronous/server.py @@ -224,7 +224,7 @@ async def run_operation( if use_cmd: first = docs[0] await operation.client._process_response(first, operation.session) # type: ignore[misc, arg-type] - _check_command_response(first, conn.max_wire_version) + _check_command_response(first, conn.max_wire_version, pool_opts=conn.opts) # type:ignore[has-type] except Exception as exc: duration = datetime.now() - start if isinstance(exc, (NotPrimaryError, OperationFailure)): diff --git a/pymongo/helpers_shared.py b/pymongo/helpers_shared.py index a664e87a69..2cfc6eb68c 100644 --- a/pymongo/helpers_shared.py +++ b/pymongo/helpers_shared.py @@ -47,6 +47,7 @@ if TYPE_CHECKING: from pymongo.cursor_shared import _Hint from pymongo.operations import _IndexList + from pymongo.pool_options import PoolOptions from pymongo.typings import _DocumentOut @@ -108,6 +109,34 @@ } +def _get_timeout_details(options: PoolOptions) -> dict[str, float]: + from pymongo import _csot + + details = {} + timeout = _csot.get_timeout() + socket_timeout = options.socket_timeout + connect_timeout = options.connect_timeout + if timeout: + details["timeoutMS"] = timeout * 1000 + if socket_timeout and not timeout: + details["socketTimeoutMS"] = socket_timeout * 1000 + if connect_timeout: + details["connectTimeoutMS"] = connect_timeout * 1000 + return details + + +def format_timeout_details(details: Optional[dict[str, float]]) -> str: + result = "" + if details: + result += " (configured timeouts:" + for timeout in ["socketTimeoutMS", "timeoutMS", "connectTimeoutMS"]: + if timeout in details: + result += f" {timeout}: {details[timeout]}ms," + result = result[:-1] + result += ")" + return result + + def _gen_index_name(keys: _IndexList) -> str: """Generate an index name from the set of fields it is over.""" return "_".join(["{}_{}".format(*item) for item in keys]) @@ -188,6 +217,7 @@ def _check_command_response( max_wire_version: Optional[int], allowable_errors: Optional[Container[Union[int, str]]] = None, parse_write_concern_error: bool = False, + pool_opts: Optional[PoolOptions] = None, ) -> None: """Check the response to a command for errors.""" if "ok" not in response: @@ -243,6 +273,10 @@ def _check_command_response( if code in (11000, 11001, 12582): raise DuplicateKeyError(errmsg, code, response, max_wire_version) elif code == 50: + # Append timeout details to MaxTimeMSExpired responses. + if pool_opts: + timeout_details = _get_timeout_details(pool_opts) + errmsg += format_timeout_details(timeout_details) raise ExecutionTimeout(errmsg, code, response, max_wire_version) elif code == 43: raise CursorNotFound(errmsg, code, response, max_wire_version) diff --git a/pymongo/pool_options.py b/pymongo/pool_options.py index 5c24709b16..a5d76007b0 100644 --- a/pymongo/pool_options.py +++ b/pymongo/pool_options.py @@ -386,8 +386,13 @@ def __init__( def _update_metadata(self, driver: DriverInfo) -> None: """Updates the client's metadata""" + if driver.name and driver.name.lower() in self.__metadata["driver"]["name"].lower().split( + "|" + ): + return metadata = copy.deepcopy(self.__metadata) + if driver.name: metadata["driver"]["name"] = "{}|{}".format( metadata["driver"]["name"], diff --git a/pymongo/pool_shared.py b/pymongo/pool_shared.py index 905f1a4d18..ac562af542 100644 --- a/pymongo/pool_shared.py +++ b/pymongo/pool_shared.py @@ -36,6 +36,7 @@ NetworkTimeout, _CertificateError, ) +from pymongo.helpers_shared import _get_timeout_details, format_timeout_details from pymongo.network_layer import AsyncNetworkingInterface, NetworkingInterface, PyMongoProtocol from pymongo.pool_options import PoolOptions from pymongo.ssl_support import PYSSLError, SSLError, _has_sni @@ -149,32 +150,6 @@ def _raise_connection_failure( raise AutoReconnect(msg) from error -def _get_timeout_details(options: PoolOptions) -> dict[str, float]: - details = {} - timeout = _csot.get_timeout() - socket_timeout = options.socket_timeout - connect_timeout = options.connect_timeout - if timeout: - details["timeoutMS"] = timeout * 1000 - if socket_timeout and not timeout: - details["socketTimeoutMS"] = socket_timeout * 1000 - if connect_timeout: - details["connectTimeoutMS"] = connect_timeout * 1000 - return details - - -def format_timeout_details(details: Optional[dict[str, float]]) -> str: - result = "" - if details: - result += " (configured timeouts:" - for timeout in ["socketTimeoutMS", "timeoutMS", "connectTimeoutMS"]: - if timeout in details: - result += f" {timeout}: {details[timeout]}ms," - result = result[:-1] - result += ")" - return result - - class _CancellationContext: def __init__(self) -> None: self._cancelled = False diff --git a/pymongo/synchronous/encryption.py b/pymongo/synchronous/encryption.py index 5f9bdac4b7..56ef84af08 100644 --- a/pymongo/synchronous/encryption.py +++ b/pymongo/synchronous/encryption.py @@ -70,12 +70,12 @@ NetworkTimeout, ServerSelectionTimeoutError, ) +from pymongo.helpers_shared import _get_timeout_details from pymongo.network_layer import sendall from pymongo.operations import UpdateOne from pymongo.pool_options import PoolOptions from pymongo.pool_shared import ( _configured_socket, - _get_timeout_details, _raise_connection_failure, ) from pymongo.read_concern import ReadConcern diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 505f58c60f..8ce1041acf 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -55,6 +55,7 @@ WaitQueueTimeoutError, ) from pymongo.hello import Hello, HelloCompat +from pymongo.helpers_shared import _get_timeout_details, format_timeout_details from pymongo.lock import ( _cond_wait, _create_condition, @@ -76,9 +77,7 @@ SSLErrors, _CancellationContext, _configured_socket_interface, - _get_timeout_details, _raise_connection_failure, - format_timeout_details, ) from pymongo.read_preferences import ReadPreference from pymongo.server_api import _add_to_command diff --git a/pymongo/synchronous/server.py b/pymongo/synchronous/server.py index c3643ba815..4322d2ad09 100644 --- a/pymongo/synchronous/server.py +++ b/pymongo/synchronous/server.py @@ -224,7 +224,7 @@ def run_operation( if use_cmd: first = docs[0] operation.client._process_response(first, operation.session) # type: ignore[misc, arg-type] - _check_command_response(first, conn.max_wire_version) + _check_command_response(first, conn.max_wire_version, pool_opts=conn.opts) # type:ignore[has-type] except Exception as exc: duration = datetime.now() - start if isinstance(exc, (NotPrimaryError, OperationFailure)): diff --git a/test/asynchronous/test_client_metadata.py b/test/asynchronous/test_client_metadata.py index cfecb49748..2f175cceed 100644 --- a/test/asynchronous/test_client_metadata.py +++ b/test/asynchronous/test_client_metadata.py @@ -107,15 +107,20 @@ async def check_metadata_added( new_name, new_version, new_platform, new_metadata = await self.send_ping_and_get_metadata( client, True ) - self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name) - self.assertEqual( - new_version, - f"{version}|{add_version}" if add_version is not None else version, - ) - self.assertEqual( - new_platform, - f"{platform}|{add_platform}" if add_platform is not None else platform, - ) + if add_name is not None and add_name.lower() in name.lower().split("|"): + self.assertEqual(name, new_name) + self.assertEqual(version, new_version) + self.assertEqual(platform, new_platform) + else: + self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name) + self.assertEqual( + new_version, + f"{version}|{add_version}" if add_version is not None else version, + ) + self.assertEqual( + new_platform, + f"{platform}|{add_platform}" if add_platform is not None else platform, + ) metadata.pop("driver") metadata.pop("platform") @@ -210,6 +215,18 @@ async def test_doesnt_update_established_connections(self): self.assertIsNone(self.handshake_req) self.assertEqual(listener.event_count(ConnectionClosedEvent), 0) + async def test_duplicate_driver_name_no_op(self): + client = await self.async_rs_or_single_client( + "mongodb://" + self.server.address_string, + maxIdleTimeMS=1, + ) + client.append_metadata(DriverInfo("library", "1.2", "Library Platform")) + await self.check_metadata_added(client, "framework", None, None) + # wait for connection to become idle + await asyncio.sleep(0.005) + # add same metadata again + await self.check_metadata_added(client, "Framework", None, None) + if __name__ == "__main__": unittest.main() diff --git a/test/asynchronous/test_collection.py b/test/asynchronous/test_collection.py index cda8452d1c..6a85b63960 100644 --- a/test/asynchronous/test_collection.py +++ b/test/asynchronous/test_collection.py @@ -335,6 +335,8 @@ async def test_create_index(self): await db.test.create_index(["hello", ("world", DESCENDING)]) await db.test.create_index({"hello": 1}.items()) # type:ignore[arg-type] + # TODO: PYTHON-5491 - remove version max + @async_client_context.require_version_max(8, 0, -1) async def test_drop_index(self): db = self.db await db.test.drop_indexes() diff --git a/test/asynchronous/test_cursor.py b/test/asynchronous/test_cursor.py index e7da40fa19..08da82762c 100644 --- a/test/asynchronous/test_cursor.py +++ b/test/asynchronous/test_cursor.py @@ -43,6 +43,7 @@ from bson import decode_all from bson.code import Code +from bson.raw_bson import RawBSONDocument from pymongo import ASCENDING, DESCENDING from pymongo.asynchronous.cursor import AsyncCursor, CursorType from pymongo.asynchronous.helpers import anext @@ -199,6 +200,21 @@ async def test_max_time_ms(self): finally: await client.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") + async def test_maxtime_ms_message(self): + db = self.db + await db.t.insert_one({"x": 1}) + with self.assertRaises(Exception) as error: + await db.t.find_one({"$where": delay(2)}, max_time_ms=1) + + self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception)) + + client = await self.async_rs_client(document_class=RawBSONDocument) + await client.db.t.insert_one({"x": 1}) + with self.assertRaises(Exception) as error: + await client.db.t.find_one({"$where": delay(2)}, max_time_ms=1) + + self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception)) + async def test_max_await_time_ms(self): db = self.db await db.pymongo_test.drop() diff --git a/test/asynchronous/test_pooling.py b/test/asynchronous/test_pooling.py index 66edf0177f..3193d9e3d5 100644 --- a/test/asynchronous/test_pooling.py +++ b/test/asynchronous/test_pooling.py @@ -21,7 +21,7 @@ import socket import sys import time -from test.asynchronous.utils import async_get_pool, async_joinall +from test.asynchronous.utils import async_get_pool, async_joinall, flaky from bson.codec_options import DEFAULT_CODEC_OPTIONS from bson.son import SON diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 964d2df96d..09bf7e83ea 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -564,6 +564,8 @@ def maybe_skip_test(self, spec): self.skipTest("CSOT not implemented for watch()") if "cursors" in class_name: self.skipTest("CSOT not implemented for cursors") + if "dropindex on collection" in description: + self.skipTest("PYTHON-5491") if ( "tailable" in class_name or "tailable" in description diff --git a/test/connection_logging/connection-logging.json b/test/connection_logging/connection-logging.json index 72103b3cab..5799e834d7 100644 --- a/test/connection_logging/connection-logging.json +++ b/test/connection_logging/connection-logging.json @@ -446,22 +446,6 @@ } } }, - { - "level": "debug", - "component": "connection", - "data": { - "message": "Connection pool cleared", - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, { "level": "debug", "component": "connection", @@ -514,6 +498,22 @@ ] } } + }, + { + "level": "debug", + "component": "connection", + "data": { + "message": "Connection pool cleared", + "serverHost": { + "$$type": "string" + }, + "serverPort": { + "$$type": [ + "int", + "long" + ] + } + } } ] } diff --git a/test/test_client_metadata.py b/test/test_client_metadata.py index 32cb9b8009..a94c5aa25e 100644 --- a/test/test_client_metadata.py +++ b/test/test_client_metadata.py @@ -107,15 +107,20 @@ def check_metadata_added( new_name, new_version, new_platform, new_metadata = self.send_ping_and_get_metadata( client, True ) - self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name) - self.assertEqual( - new_version, - f"{version}|{add_version}" if add_version is not None else version, - ) - self.assertEqual( - new_platform, - f"{platform}|{add_platform}" if add_platform is not None else platform, - ) + if add_name is not None and add_name.lower() in name.lower().split("|"): + self.assertEqual(name, new_name) + self.assertEqual(version, new_version) + self.assertEqual(platform, new_platform) + else: + self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name) + self.assertEqual( + new_version, + f"{version}|{add_version}" if add_version is not None else version, + ) + self.assertEqual( + new_platform, + f"{platform}|{add_platform}" if add_platform is not None else platform, + ) metadata.pop("driver") metadata.pop("platform") @@ -210,6 +215,18 @@ def test_doesnt_update_established_connections(self): self.assertIsNone(self.handshake_req) self.assertEqual(listener.event_count(ConnectionClosedEvent), 0) + def test_duplicate_driver_name_no_op(self): + client = self.rs_or_single_client( + "mongodb://" + self.server.address_string, + maxIdleTimeMS=1, + ) + client.append_metadata(DriverInfo("library", "1.2", "Library Platform")) + self.check_metadata_added(client, "framework", None, None) + # wait for connection to become idle + time.sleep(0.005) + # add same metadata again + self.check_metadata_added(client, "Framework", None, None) + if __name__ == "__main__": unittest.main() diff --git a/test/test_collection.py b/test/test_collection.py index ccace72bec..0dce88423b 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -333,6 +333,8 @@ def test_create_index(self): db.test.create_index(["hello", ("world", DESCENDING)]) db.test.create_index({"hello": 1}.items()) # type:ignore[arg-type] + # TODO: PYTHON-5491 - remove version max + @client_context.require_version_max(8, 0, -1) def test_drop_index(self): db = self.db db.test.drop_indexes() diff --git a/test/test_cursor.py b/test/test_cursor.py index 9a4fb86e93..b63638bfab 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -43,6 +43,7 @@ from bson import decode_all from bson.code import Code +from bson.raw_bson import RawBSONDocument from pymongo import ASCENDING, DESCENDING from pymongo.collation import Collation from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure, PyMongoError @@ -197,6 +198,21 @@ def test_max_time_ms(self): finally: client.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") + def test_maxtime_ms_message(self): + db = self.db + db.t.insert_one({"x": 1}) + with self.assertRaises(Exception) as error: + db.t.find_one({"$where": delay(2)}, max_time_ms=1) + + self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception)) + + client = self.rs_client(document_class=RawBSONDocument) + client.db.t.insert_one({"x": 1}) + with self.assertRaises(Exception) as error: + client.db.t.find_one({"$where": delay(2)}, max_time_ms=1) + + self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception)) + def test_max_await_time_ms(self): db = self.db db.pymongo_test.drop() diff --git a/test/test_pooling.py b/test/test_pooling.py index b995c467c2..cb5b206996 100644 --- a/test/test_pooling.py +++ b/test/test_pooling.py @@ -21,7 +21,7 @@ import socket import sys import time -from test.utils import get_pool, joinall +from test.utils import flaky, get_pool, joinall from bson.codec_options import DEFAULT_CODEC_OPTIONS from bson.son import SON diff --git a/test/unified_format.py b/test/unified_format.py index c21f29fe19..3496b2ad44 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -563,6 +563,8 @@ def maybe_skip_test(self, spec): self.skipTest("CSOT not implemented for watch()") if "cursors" in class_name: self.skipTest("CSOT not implemented for cursors") + if "dropindex on collection" in description: + self.skipTest("PYTHON-5491") if ( "tailable" in class_name or "tailable" in description