Compare commits

...

15 Commits

Author SHA1 Message Date
mongodb-dbx-release-bot[bot]
27fac499c1
BUMP 4.14.2.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-08-19 18:26:36 +00:00
Iris
0d2a4b462e
Prep for 4.14.1 release (#2495) 2025-08-19 10:59:54 -07:00
mongodb-dbx-release-bot[bot]
550d234f71
PYTHON-5503 Use uv to install just in GitHub Actions (#2490) [v4.14] (#2493)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-08-19 11:38:30 -05:00
Steven Silvester
d98049c630
PYTHON-5502 [v4.14] Fix handling of c extensions in Azure and GCP VMs (#2487) 2025-08-19 11:27:51 -05:00
mongodb-dbx-release-bot[bot]
f66ec0ff23
PYTHON-5492 Fix handling of MaxTimeMS message (#2484) [v4.14] (#2485)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-08-18 20:28:27 -05:00
Steven Silvester
6611bec9e7
PYTHON-5493 [v4.14] Add a patch for the log order difference (#2482) 2025-08-18 15:03:15 -05:00
mongodb-dbx-release-bot[bot]
7692bd67b9
PYTHON-5488 append_metadata should not add duplicates (#2461) [v4.14] (#2483)
Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com>
2025-08-18 15:03:02 -05:00
mongodb-dbx-release-bot[bot]
aa0b920566
PYTHON-5492 Fix handling of MaxTimeMSExpired responses (#2477) [v4.14] (#2479)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-08-18 06:32:51 -05:00
mongodb-dbx-release-bot[bot]
1c480163f4
PYTHON-5349 Use drivers-evergreen-tools to start servers in GitHub Actions (#2474) [v4.14] (#2476)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-08-15 12:03:01 -05:00
mongodb-dbx-release-bot[bot]
7055ad1944
PYTHON-5492 Mark test as flaky (#2472) [v4.14] (#2475)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-08-15 12:01:53 -05:00
mongodb-dbx-release-bot[bot]
e80f4f4199
PYTHON-5491 Skip non-idempotent dropIndex tests (#2467) [v4.14] (#2468)
Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
2025-08-11 08:27:21 -05:00
Steven Silvester
c4e866d5d7
PYTHON-5487 [v4.14] Update 4.14 changelog to mention MongoDB 4.0 is no longer supported (#2464)
Co-authored-by: Jeffrey A. Clark <aclark@aclark.net>
Co-authored-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-08-07 13:26:50 -05:00
mongodb-dbx-release-bot[bot]
ce2812d974
Prep branch v4.14
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-08-07 15:40:53 +00:00
mongodb-dbx-release-bot[bot]
952fa82c24
BUMP 4.15.0.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-08-06 13:44:58 +00:00
Jeffrey A. Clark
6354d6cae7
Prepare 4.14.0 release (#2458) 2025-08-06 08:52:42 -04:00
32 changed files with 383 additions and 118 deletions

View File

@ -535,6 +535,8 @@ buildvariants:
display_name: "* MongoDB v4.2" display_name: "* MongoDB v4.2"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: "4.2"
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-v4.4 - name: mongodb-v4.4
tasks: tasks:
@ -542,6 +544,8 @@ buildvariants:
display_name: "* MongoDB v4.4" display_name: "* MongoDB v4.4"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: "4.4"
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-v5.0 - name: mongodb-v5.0
tasks: tasks:
@ -549,6 +553,8 @@ buildvariants:
display_name: "* MongoDB v5.0" display_name: "* MongoDB v5.0"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: "5.0"
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-v6.0 - name: mongodb-v6.0
tasks: tasks:
@ -556,6 +562,8 @@ buildvariants:
display_name: "* MongoDB v6.0" display_name: "* MongoDB v6.0"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: "6.0"
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-v7.0 - name: mongodb-v7.0
tasks: tasks:
@ -563,6 +571,8 @@ buildvariants:
display_name: "* MongoDB v7.0" display_name: "* MongoDB v7.0"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: "7.0"
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-v8.0 - name: mongodb-v8.0
tasks: tasks:
@ -570,6 +580,8 @@ buildvariants:
display_name: "* MongoDB v8.0" display_name: "* MongoDB v8.0"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: "8.0"
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-rapid - name: mongodb-rapid
tasks: tasks:
@ -577,6 +589,8 @@ buildvariants:
display_name: "* MongoDB rapid" display_name: "* MongoDB rapid"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: rapid
tags: [coverage_tag] tags: [coverage_tag]
- name: mongodb-latest - name: mongodb-latest
tasks: tasks:
@ -584,6 +598,8 @@ buildvariants:
display_name: "* MongoDB latest" display_name: "* MongoDB latest"
run_on: run_on:
- rhel87-small - rhel87-small
expansions:
VERSION: latest
tags: [coverage_tag] tags: [coverage_tag]
# Stable api tests # Stable api tests

View File

@ -8,7 +8,9 @@ if [ ${OIDC_ENV} == "k8s" ]; then
SUB_TEST_NAME=$K8S_VARIANT-remote SUB_TEST_NAME=$K8S_VARIANT-remote
else else
SUB_TEST_NAME=$OIDC_ENV-remote SUB_TEST_NAME=$OIDC_ENV-remote
sudo apt-get install -y python3-dev build-essential
fi fi
bash ./.evergreen/just.sh setup-tests auth_oidc $SUB_TEST_NAME bash ./.evergreen/just.sh setup-tests auth_oidc $SUB_TEST_NAME
bash ./.evergreen/just.sh run-tests "${@:1}" bash ./.evergreen/just.sh run-tests "${@:1}"

View File

@ -74,7 +74,11 @@ def create_server_version_variants() -> list[BuildVariant]:
for version in ALL_VERSIONS: for version in ALL_VERSIONS:
display_name = get_variant_name("* MongoDB", version=version) display_name = get_variant_name("* MongoDB", version=version)
variant = create_variant( 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) variants.append(variant)
return variants return variants

View File

@ -48,9 +48,12 @@ if ! command -v just &>/dev/null; then
_TARGET="--target x86_64-pc-windows-msvc" _TARGET="--target x86_64-pc-windows-msvc"
fi fi
_BIN_DIR=$PYMONGO_BIN_DIR _BIN_DIR=$PYMONGO_BIN_DIR
mkdir -p ${_BIN_DIR}
echo "Installing just..." echo "Installing just..."
mkdir -p "$_BIN_DIR" 2>/dev/null || true 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" || { 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 _pip_install rust-just just
} }
echo "Installing just... done." echo "Installing just... done."
@ -59,6 +62,7 @@ fi
# Ensure uv is installed. # Ensure uv is installed.
if ! command -v uv &>/dev/null; then if ! command -v uv &>/dev/null; then
_BIN_DIR=$PYMONGO_BIN_DIR _BIN_DIR=$PYMONGO_BIN_DIR
mkdir -p ${_BIN_DIR}
echo "Installing uv..." echo "Installing uv..."
# On most systems we can install directly. # 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 || { curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$_BIN_DIR" INSTALLER_NO_MODIFY_PATH=1 sh || {

View File

@ -30,6 +30,9 @@ def _setup_azure_vm(base_env: dict[str, str]) -> None:
env["AZUREKMS_CMD"] = "tar xf mongo-python-driver.tgz" env["AZUREKMS_CMD"] = "tar xf mongo-python-driver.tgz"
run_command(f"{azure_dir}/run-command.sh", env=env) 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" env["AZUREKMS_CMD"] = "bash .evergreen/just.sh setup-tests kms azure-remote"
run_command(f"{azure_dir}/run-command.sh", env=env) run_command(f"{azure_dir}/run-command.sh", env=env)
LOGGER.info("Setting up Azure VM... done.") 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" env["GCPKMS_CMD"] = "tar xf mongo-python-driver.tgz"
run_command(f"{gcp_dir}/run-command.sh", env=env) 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" env["GCPKMS_CMD"] = "bash ./.evergreen/just.sh setup-tests kms gcp-remote"
run_command(f"{gcp_dir}/run-command.sh", env=env) run_command(f"{gcp_dir}/run-command.sh", env=env)
LOGGER.info("Setting up GCP VM...") LOGGER.info("Setting up GCP VM...")

View File

@ -2,7 +2,14 @@ from __future__ import annotations
import os 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_NAMES = ["aks", "gke", "eks"]
K8S_REMOTE_NAMES = [f"{n}-remote" for n in K8S_NAMES] K8S_REMOTE_NAMES = [f"{n}-remote" for n in K8S_NAMES]

View File

@ -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"
+ ]
+ }
+ }
}
]
}

View File

@ -16,7 +16,7 @@ env:
# Changes per repo # Changes per repo
PRODUCT_NAME: PyMongo PRODUCT_NAME: PyMongo
# Changes per branch # Changes per branch
EVERGREEN_PROJECT: mongo-python-driver EVERGREEN_PROJECT: mongo-python-driver-release
# Constant # Constant
# inputs will be empty on a scheduled run. so, we only set dry_run # inputs will be empty on a scheduled run. so, we only set dry_run
# to 'false' when the input is set to 'false'. # to 'false' when the input is set to 'false'.

View File

@ -22,13 +22,13 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Install just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5
with: with:
enable-cache: true enable-cache: true
python-version: "3.9" python-version: "3.9"
- name: Install just
run: uv tool install rust-just
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
just install just install
@ -50,33 +50,31 @@ jobs:
cppcheck pymongo cppcheck pymongo
build: build:
# supercharge/mongodb-github-action requires containers so we don't test other platforms
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Tests currently only pass on ubuntu on GitHub Actions.
os: [ubuntu-latest] 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 }} name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Install just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5
with: with:
enable-cache: true enable-cache: true
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - id: setup-mongodb
run: just install uses: mongodb-labs/drivers-evergreen-tools@master
- name: Start MongoDB
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0
with: with:
mongodb-version: 6.0 version: "${{ matrix.mongodb-version }}"
- name: Run tests - name: Run tests
run: just test run: uv run --extra test pytest -v
doctest: doctest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -85,17 +83,17 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Install just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5
with: with:
enable-cache: true enable-cache: true
python-version: "3.9" python-version: "3.9"
- name: Start MongoDB - name: Install just
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 run: uv tool install rust-just
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
with: with:
mongodb-version: '8.0.0-rc4' version: "8.0"
- name: Install dependencies - name: Install dependencies
run: just install run: just install
- name: Run tests - name: Run tests
@ -116,7 +114,7 @@ jobs:
enable-cache: true enable-cache: true
python-version: "3.9" python-version: "3.9"
- name: Install just - name: Install just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 run: uv tool install rust-just
- name: Install dependencies - name: Install dependencies
run: just install run: just install
- name: Build docs - name: Build docs
@ -135,7 +133,7 @@ jobs:
enable-cache: true enable-cache: true
python-version: "3.9" python-version: "3.9"
- name: Install just - name: Install just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 run: uv tool install rust-just
- name: Install dependencies - name: Install dependencies
run: just install run: just install
- name: Build docs - name: Build docs
@ -157,7 +155,7 @@ jobs:
enable-cache: true enable-cache: true
python-version: "${{matrix.python}}" python-version: "${{matrix.python}}"
- name: Install just - name: Install just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 run: uv tool install rust-just
- name: Install dependencies - name: Install dependencies
run: | run: |
just install just install
@ -210,8 +208,8 @@ jobs:
cache-dependency-path: 'sdist/test/pyproject.toml' cache-dependency-path: 'sdist/test/pyproject.toml'
# Test sdist on lowest supported Python # Test sdist on lowest supported Python
python-version: '3.9' python-version: '3.9'
- name: Start MongoDB - id: setup-mongodb
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 uses: mongodb-labs/drivers-evergreen-tools@master
- name: Run connect test from sdist - name: Run connect test from sdist
shell: bash shell: bash
run: | run: |
@ -234,10 +232,10 @@ jobs:
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5
with: with:
python-version: '3.9' python-version: '3.9'
- name: Start MongoDB - id: setup-mongodb
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 uses: mongodb-labs/drivers-evergreen-tools@master
with: 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 # Async and our test_dns do not support dnspython 1.X, so we don't run async or dns tests here
- name: Run tests - name: Run tests
shell: bash shell: bash
@ -260,10 +258,10 @@ jobs:
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5 uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v5
with: with:
python-version: '3.9' python-version: '3.9'
- name: Start MongoDB - id: setup-mongodb
uses: supercharge/mongodb-github-action@90004df786821b6308fb02299e5835d0dae05d0d # 1.12.0 uses: mongodb-labs/drivers-evergreen-tools@master
with: 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 # The lifetime kwarg we use in srv resolution was added to the async resolver API in dnspython 2.1.0
- name: Run tests - name: Run tests
shell: bash shell: bash

7
.github/zizmor.yml vendored Normal file
View File

@ -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

View File

@ -1,25 +1,61 @@
Changelog Changelog
========= =========
Changes in Version 4.14.1 (2025/08/19)
Changes in Version 4.14.0 (XXXX/XX/XX)
-------------------------------------- --------------------------------------
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 (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: 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: - Dropped support for MongoDB 4.0.
- Subinterpreters (``concurrent.interpreters``) - Added preliminary support for Python 3.14 and 3.14 with free-threading. We do
- Free-threading with Encryption not yet support the following with Python 3.14:
- 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
- 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 - Subinterpreters (``concurrent.interpreters``)
if the 'padding' metadata field is < 0 or > 7, or non-zero for any type other than PACKED_BIT. - Free-threading with Encryption
- Changed :meth:`~pymongo.uri_parser.parse_uri`'s ``options`` parameter to be type ``dict`` instead of ``_CaseInsensitiveDictionary``. - 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
- 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.
.. _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) Changes in Version 4.13.2 (2025/06/17)
-------------------------------------- --------------------------------------

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import re import re
from typing import List, Tuple, Union 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], ...]: def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]:

View File

@ -75,12 +75,12 @@ from pymongo.errors import (
NetworkTimeout, NetworkTimeout,
ServerSelectionTimeoutError, ServerSelectionTimeoutError,
) )
from pymongo.helpers_shared import _get_timeout_details
from pymongo.network_layer import async_socket_sendall from pymongo.network_layer import async_socket_sendall
from pymongo.operations import UpdateOne from pymongo.operations import UpdateOne
from pymongo.pool_options import PoolOptions from pymongo.pool_options import PoolOptions
from pymongo.pool_shared import ( from pymongo.pool_shared import (
_async_configured_socket, _async_configured_socket,
_get_timeout_details,
_raise_connection_failure, _raise_connection_failure,
) )
from pymongo.read_concern import ReadConcern from pymongo.read_concern import ReadConcern

View File

@ -58,6 +58,7 @@ from pymongo.errors import ( # type:ignore[attr-defined]
WaitQueueTimeoutError, WaitQueueTimeoutError,
) )
from pymongo.hello import Hello, HelloCompat from pymongo.hello import Hello, HelloCompat
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
from pymongo.lock import ( from pymongo.lock import (
_async_cond_wait, _async_cond_wait,
_async_create_condition, _async_create_condition,
@ -79,9 +80,7 @@ from pymongo.pool_shared import (
SSLErrors, SSLErrors,
_CancellationContext, _CancellationContext,
_configured_protocol_interface, _configured_protocol_interface,
_get_timeout_details,
_raise_connection_failure, _raise_connection_failure,
format_timeout_details,
) )
from pymongo.read_preferences import ReadPreference from pymongo.read_preferences import ReadPreference
from pymongo.server_api import _add_to_command from pymongo.server_api import _add_to_command

View File

@ -224,7 +224,7 @@ class Server:
if use_cmd: if use_cmd:
first = docs[0] first = docs[0]
await operation.client._process_response(first, operation.session) # type: ignore[misc, arg-type] 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: except Exception as exc:
duration = datetime.now() - start duration = datetime.now() - start
if isinstance(exc, (NotPrimaryError, OperationFailure)): if isinstance(exc, (NotPrimaryError, OperationFailure)):

View File

@ -47,6 +47,7 @@ from pymongo.hello import HelloCompat
if TYPE_CHECKING: if TYPE_CHECKING:
from pymongo.cursor_shared import _Hint from pymongo.cursor_shared import _Hint
from pymongo.operations import _IndexList from pymongo.operations import _IndexList
from pymongo.pool_options import PoolOptions
from pymongo.typings import _DocumentOut from pymongo.typings import _DocumentOut
@ -108,6 +109,34 @@ _SENSITIVE_COMMANDS: set = {
} }
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: def _gen_index_name(keys: _IndexList) -> str:
"""Generate an index name from the set of fields it is over.""" """Generate an index name from the set of fields it is over."""
return "_".join(["{}_{}".format(*item) for item in keys]) return "_".join(["{}_{}".format(*item) for item in keys])
@ -188,6 +217,7 @@ def _check_command_response(
max_wire_version: Optional[int], max_wire_version: Optional[int],
allowable_errors: Optional[Container[Union[int, str]]] = None, allowable_errors: Optional[Container[Union[int, str]]] = None,
parse_write_concern_error: bool = False, parse_write_concern_error: bool = False,
pool_opts: Optional[PoolOptions] = None,
) -> None: ) -> None:
"""Check the response to a command for errors.""" """Check the response to a command for errors."""
if "ok" not in response: if "ok" not in response:
@ -243,6 +273,10 @@ def _check_command_response(
if code in (11000, 11001, 12582): if code in (11000, 11001, 12582):
raise DuplicateKeyError(errmsg, code, response, max_wire_version) raise DuplicateKeyError(errmsg, code, response, max_wire_version)
elif code == 50: 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) raise ExecutionTimeout(errmsg, code, response, max_wire_version)
elif code == 43: elif code == 43:
raise CursorNotFound(errmsg, code, response, max_wire_version) raise CursorNotFound(errmsg, code, response, max_wire_version)

View File

@ -386,8 +386,13 @@ class PoolOptions:
def _update_metadata(self, driver: DriverInfo) -> None: def _update_metadata(self, driver: DriverInfo) -> None:
"""Updates the client's metadata""" """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) metadata = copy.deepcopy(self.__metadata)
if driver.name: if driver.name:
metadata["driver"]["name"] = "{}|{}".format( metadata["driver"]["name"] = "{}|{}".format(
metadata["driver"]["name"], metadata["driver"]["name"],

View File

@ -36,6 +36,7 @@ from pymongo.errors import ( # type:ignore[attr-defined]
NetworkTimeout, NetworkTimeout,
_CertificateError, _CertificateError,
) )
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
from pymongo.network_layer import AsyncNetworkingInterface, NetworkingInterface, PyMongoProtocol from pymongo.network_layer import AsyncNetworkingInterface, NetworkingInterface, PyMongoProtocol
from pymongo.pool_options import PoolOptions from pymongo.pool_options import PoolOptions
from pymongo.ssl_support import PYSSLError, SSLError, _has_sni from pymongo.ssl_support import PYSSLError, SSLError, _has_sni
@ -149,32 +150,6 @@ def _raise_connection_failure(
raise AutoReconnect(msg) from error 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: class _CancellationContext:
def __init__(self) -> None: def __init__(self) -> None:
self._cancelled = False self._cancelled = False

View File

@ -70,12 +70,12 @@ from pymongo.errors import (
NetworkTimeout, NetworkTimeout,
ServerSelectionTimeoutError, ServerSelectionTimeoutError,
) )
from pymongo.helpers_shared import _get_timeout_details
from pymongo.network_layer import sendall from pymongo.network_layer import sendall
from pymongo.operations import UpdateOne from pymongo.operations import UpdateOne
from pymongo.pool_options import PoolOptions from pymongo.pool_options import PoolOptions
from pymongo.pool_shared import ( from pymongo.pool_shared import (
_configured_socket, _configured_socket,
_get_timeout_details,
_raise_connection_failure, _raise_connection_failure,
) )
from pymongo.read_concern import ReadConcern from pymongo.read_concern import ReadConcern

View File

@ -55,6 +55,7 @@ from pymongo.errors import ( # type:ignore[attr-defined]
WaitQueueTimeoutError, WaitQueueTimeoutError,
) )
from pymongo.hello import Hello, HelloCompat from pymongo.hello import Hello, HelloCompat
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
from pymongo.lock import ( from pymongo.lock import (
_cond_wait, _cond_wait,
_create_condition, _create_condition,
@ -76,9 +77,7 @@ from pymongo.pool_shared import (
SSLErrors, SSLErrors,
_CancellationContext, _CancellationContext,
_configured_socket_interface, _configured_socket_interface,
_get_timeout_details,
_raise_connection_failure, _raise_connection_failure,
format_timeout_details,
) )
from pymongo.read_preferences import ReadPreference from pymongo.read_preferences import ReadPreference
from pymongo.server_api import _add_to_command from pymongo.server_api import _add_to_command

View File

@ -224,7 +224,7 @@ class Server:
if use_cmd: if use_cmd:
first = docs[0] first = docs[0]
operation.client._process_response(first, operation.session) # type: ignore[misc, arg-type] 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: except Exception as exc:
duration = datetime.now() - start duration = datetime.now() - start
if isinstance(exc, (NotPrimaryError, OperationFailure)): if isinstance(exc, (NotPrimaryError, OperationFailure)):

View File

@ -107,15 +107,20 @@ class TestClientMetadataProse(AsyncIntegrationTest):
new_name, new_version, new_platform, new_metadata = await self.send_ping_and_get_metadata( new_name, new_version, new_platform, new_metadata = await self.send_ping_and_get_metadata(
client, True client, True
) )
self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name) if add_name is not None and add_name.lower() in name.lower().split("|"):
self.assertEqual( self.assertEqual(name, new_name)
new_version, self.assertEqual(version, new_version)
f"{version}|{add_version}" if add_version is not None else version, self.assertEqual(platform, new_platform)
) else:
self.assertEqual( self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name)
new_platform, self.assertEqual(
f"{platform}|{add_platform}" if add_platform is not None else platform, 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("driver")
metadata.pop("platform") metadata.pop("platform")
@ -210,6 +215,18 @@ class TestClientMetadataProse(AsyncIntegrationTest):
self.assertIsNone(self.handshake_req) self.assertIsNone(self.handshake_req)
self.assertEqual(listener.event_count(ConnectionClosedEvent), 0) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -335,6 +335,8 @@ class AsyncTestCollection(AsyncIntegrationTest):
await db.test.create_index(["hello", ("world", DESCENDING)]) await db.test.create_index(["hello", ("world", DESCENDING)])
await db.test.create_index({"hello": 1}.items()) # type:ignore[arg-type] 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): async def test_drop_index(self):
db = self.db db = self.db
await db.test.drop_indexes() await db.test.drop_indexes()

View File

@ -43,6 +43,7 @@ from test.utils_shared import (
from bson import decode_all from bson import decode_all
from bson.code import Code from bson.code import Code
from bson.raw_bson import RawBSONDocument
from pymongo import ASCENDING, DESCENDING from pymongo import ASCENDING, DESCENDING
from pymongo.asynchronous.cursor import AsyncCursor, CursorType from pymongo.asynchronous.cursor import AsyncCursor, CursorType
from pymongo.asynchronous.helpers import anext from pymongo.asynchronous.helpers import anext
@ -199,6 +200,21 @@ class TestCursor(AsyncIntegrationTest):
finally: finally:
await client.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") 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): async def test_max_await_time_ms(self):
db = self.db db = self.db
await db.pymongo_test.drop() await db.pymongo_test.drop()

View File

@ -21,7 +21,7 @@ import random
import socket import socket
import sys import sys
import time 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.codec_options import DEFAULT_CODEC_OPTIONS
from bson.son import SON from bson.son import SON

View File

@ -564,6 +564,8 @@ class UnifiedSpecTestMixinV1(AsyncIntegrationTest):
self.skipTest("CSOT not implemented for watch()") self.skipTest("CSOT not implemented for watch()")
if "cursors" in class_name: if "cursors" in class_name:
self.skipTest("CSOT not implemented for cursors") self.skipTest("CSOT not implemented for cursors")
if "dropindex on collection" in description:
self.skipTest("PYTHON-5491")
if ( if (
"tailable" in class_name "tailable" in class_name
or "tailable" in description or "tailable" in description

View File

@ -446,22 +446,6 @@
} }
} }
}, },
{
"level": "debug",
"component": "connection",
"data": {
"message": "Connection pool cleared",
"serverHost": {
"$$type": "string"
},
"serverPort": {
"$$type": [
"int",
"long"
]
}
}
},
{ {
"level": "debug", "level": "debug",
"component": "connection", "component": "connection",
@ -514,6 +498,22 @@
] ]
} }
} }
},
{
"level": "debug",
"component": "connection",
"data": {
"message": "Connection pool cleared",
"serverHost": {
"$$type": "string"
},
"serverPort": {
"$$type": [
"int",
"long"
]
}
}
} }
] ]
} }

View File

@ -107,15 +107,20 @@ class TestClientMetadataProse(IntegrationTest):
new_name, new_version, new_platform, new_metadata = self.send_ping_and_get_metadata( new_name, new_version, new_platform, new_metadata = self.send_ping_and_get_metadata(
client, True client, True
) )
self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name) if add_name is not None and add_name.lower() in name.lower().split("|"):
self.assertEqual( self.assertEqual(name, new_name)
new_version, self.assertEqual(version, new_version)
f"{version}|{add_version}" if add_version is not None else version, self.assertEqual(platform, new_platform)
) else:
self.assertEqual( self.assertEqual(new_name, f"{name}|{add_name}" if add_name is not None else name)
new_platform, self.assertEqual(
f"{platform}|{add_platform}" if add_platform is not None else platform, 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("driver")
metadata.pop("platform") metadata.pop("platform")
@ -210,6 +215,18 @@ class TestClientMetadataProse(IntegrationTest):
self.assertIsNone(self.handshake_req) self.assertIsNone(self.handshake_req)
self.assertEqual(listener.event_count(ConnectionClosedEvent), 0) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -333,6 +333,8 @@ class TestCollection(IntegrationTest):
db.test.create_index(["hello", ("world", DESCENDING)]) db.test.create_index(["hello", ("world", DESCENDING)])
db.test.create_index({"hello": 1}.items()) # type:ignore[arg-type] 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): def test_drop_index(self):
db = self.db db = self.db
db.test.drop_indexes() db.test.drop_indexes()

View File

@ -43,6 +43,7 @@ from test.utils_shared import (
from bson import decode_all from bson import decode_all
from bson.code import Code from bson.code import Code
from bson.raw_bson import RawBSONDocument
from pymongo import ASCENDING, DESCENDING from pymongo import ASCENDING, DESCENDING
from pymongo.collation import Collation from pymongo.collation import Collation
from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure, PyMongoError from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure, PyMongoError
@ -197,6 +198,21 @@ class TestCursor(IntegrationTest):
finally: finally:
client.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off") 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): def test_max_await_time_ms(self):
db = self.db db = self.db
db.pymongo_test.drop() db.pymongo_test.drop()

View File

@ -21,7 +21,7 @@ import random
import socket import socket
import sys import sys
import time 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.codec_options import DEFAULT_CODEC_OPTIONS
from bson.son import SON from bson.son import SON

View File

@ -563,6 +563,8 @@ class UnifiedSpecTestMixinV1(IntegrationTest):
self.skipTest("CSOT not implemented for watch()") self.skipTest("CSOT not implemented for watch()")
if "cursors" in class_name: if "cursors" in class_name:
self.skipTest("CSOT not implemented for cursors") self.skipTest("CSOT not implemented for cursors")
if "dropindex on collection" in description:
self.skipTest("PYTHON-5491")
if ( if (
"tailable" in class_name "tailable" in class_name
or "tailable" in description or "tailable" in description