Merge branch 'master' of github.com:mongodb/mongo-python-driver into backpressure

This commit is contained in:
Steven Silvester 2026-01-14 10:41:43 -06:00
commit 84699d284b
No known key found for this signature in database
185 changed files with 4904 additions and 2969 deletions

View File

@ -239,6 +239,19 @@ functions:
working_dir: src
type: test
# Test numpy
test numpy:
- command: subprocess.exec
params:
binary: bash
args:
- .evergreen/just.sh
- test-numpy
working_dir: src
include_expansions_in_env:
- TOOLCHAIN_VERSION
type: test
# Upload coverage
upload coverage:
- command: ec2.assume_role

File diff suppressed because it is too large Load Diff

View File

@ -177,7 +177,6 @@ buildvariants:
- name: encryption-rhel8
tasks:
- name: .test-non-standard
- name: .test-min-deps
display_name: Encryption RHEL8
run_on:
- rhel87-small
@ -208,7 +207,6 @@ buildvariants:
- name: encryption-crypt_shared-rhel8
tasks:
- name: .test-non-standard
- name: .test-min-deps
display_name: Encryption crypt_shared RHEL8
run_on:
- rhel87-small
@ -326,6 +324,14 @@ buildvariants:
expansions:
TEST_NAME: load_balancer
# Min support tests
- name: min-support-rhel8
tasks:
- name: .test-min-support
display_name: Min Support RHEL8
run_on:
- rhel87-small
# Mockupdb tests
- name: mockupdb-rhel8
tasks:
@ -621,3 +627,42 @@ buildvariants:
- rhel87-small
expansions:
STORAGE_ENGINE: inmemory
# Test numpy tests
- name: test-numpy-rhel8
tasks:
- name: .test-numpy
display_name: Test Numpy RHEL8
run_on:
- rhel87-small
tags: [binary, vector, pr]
- name: test-numpy-macos
tasks:
- name: .test-numpy
display_name: Test Numpy macOS
run_on:
- macos-14
tags: [binary, vector]
- name: test-numpy-macos-arm64
tasks:
- name: .test-numpy
display_name: Test Numpy macOS Arm64
run_on:
- macos-14-arm64
tags: [binary, vector]
- name: test-numpy-win64
tasks:
- name: .test-numpy
display_name: Test Numpy Win64
run_on:
- windows-64-vsMulti-small
tags: [binary, vector]
- name: test-numpy-win32
tasks:
- name: .test-numpy
display_name: Test Numpy Win32
run_on:
- windows-64-vsMulti-small
expansions:
IS_WIN32: "1"
tags: [binary, vector]

View File

@ -50,4 +50,7 @@ rm $PYMONGO/test/csot/override-database-timeoutMS.json
# PYTHON-2943 - Socks5 Proxy Support
rm $PYMONGO/test/uri_options/proxy-options.json
# PYTHON-5517 - Avoid clearing the connection pool when the server connection rate limiter triggers
rm $PYMONGO/test/discovery_and_monitoring/unified/backpressure-*.json
echo "Done removing unimplemented tests"

View File

@ -128,7 +128,7 @@ def create_encryption_variants() -> list[BuildVariant]:
):
expansions = get_encryption_expansions(encryption)
display_name = get_variant_name(encryption, host, **expansions)
tasks = [".test-non-standard", ".test-min-deps"]
tasks = [".test-non-standard"]
if host != "rhel8":
tasks = [".test-non-standard !.pypy"]
variant = create_variant(
@ -339,6 +339,37 @@ def create_disable_test_commands_variants():
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
def create_test_numpy_tasks():
tasks = []
for python in MIN_MAX_PYTHON:
tags = ["binary", "vector", f"python-{python}", "test-numpy"]
task_name = get_task_name("test-numpy", python=python)
test_func = FunctionCall(func="test numpy", vars=dict(TOOLCHAIN_VERSION=python))
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
return tasks
def create_test_numpy_variants() -> list[BuildVariant]:
variants = []
base_display_name = "Test Numpy"
# Test a subset on each of the other platforms.
for host_name in ("rhel8", "macos", "macos-arm64", "win64", "win32"):
tasks = [".test-numpy"]
host = HOSTS[host_name]
tags = ["binary", "vector"]
if host_name == "rhel8":
tags.append("pr")
expansions = dict()
if host_name == "win32":
expansions["IS_WIN32"] = "1"
display_name = get_variant_name(base_display_name, host)
variant = create_variant(tasks, display_name, host=host, tags=tags, expansions=expansions)
variants.append(variant)
return variants
def create_oidc_auth_variants():
variants = []
for host_name in ["ubuntu22", "macos", "win64"]:
@ -471,6 +502,12 @@ def create_aws_auth_variants():
return variants
def create_min_support_variants():
host = HOSTS["rhel8"]
name = get_variant_name("Min Support", host=host)
return [create_variant([".test-min-support"], name, host=host)]
def create_no_server_variants():
host = HOSTS["rhel8"]
name = get_variant_name("No server", host=host)
@ -544,6 +581,8 @@ def create_server_version_tasks():
seen.add(combo)
tags.append("pr")
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
if python == ALL_PYTHONS[0]:
expansions["TEST_MIN_DEPS"] = "1"
if "t" in python:
tags.append("free-threaded")
if python not in PYPYS and "t" not in python:
@ -609,6 +648,8 @@ def create_test_non_standard_tasks():
if pr:
tags.append("pr")
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
if python == ALL_PYTHONS[0]:
expansions["TEST_MIN_DEPS"] = "1"
name = get_task_name("test-non-standard", python=python, **expansions)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
@ -649,6 +690,8 @@ def create_test_standard_auth_tasks():
if pr:
tags.append("pr")
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
if python == ALL_PYTHONS[0]:
expansions["TEST_MIN_DEPS"] = "1"
name = get_task_name("test-standard-auth", python=python, **expansions)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
@ -658,22 +701,6 @@ def create_test_standard_auth_tasks():
return tasks
def create_min_deps_tasks():
"""For variants that support testing with minimum dependencies."""
tasks = []
for topology in TOPOLOGIES:
auth, ssl = get_standard_auth_ssl(topology)
tags = ["test-min-deps", f"{topology}-{auth}-{ssl}"]
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
test_vars["TEST_MIN_DEPS"] = "1"
name = get_task_name("test-min-deps", python=CPYTHONS[0], sync="sync", **test_vars)
test_func = FunctionCall(func="run tests", vars=test_vars)
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
return tasks
def create_standard_tasks():
"""For variants that do not set a TEST_NAME."""
tasks = []
@ -701,6 +728,8 @@ def create_standard_tasks():
if pr:
tags.append("pr")
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
if python == ALL_PYTHONS[0]:
expansions["TEST_MIN_DEPS"] = "1"
name = get_task_name("test-standard", python=python, sync=sync, **expansions)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
@ -718,9 +747,11 @@ def create_no_orchestration_tasks():
"test-no-orchestration",
f"python-{python}",
]
name = get_task_name("test-no-orchestration", python=python)
assume_func = FunctionCall(func="assume ec2 role")
test_vars = dict(TOOLCHAIN_VERSION=python)
if python == ALL_PYTHONS[0]:
test_vars["TEST_MIN_DEPS"] = "1"
name = get_task_name("test-no-orchestration", **test_vars)
test_func = FunctionCall(func="run tests", vars=test_vars)
commands = [assume_func, test_func]
tasks.append(EvgTask(name=name, tags=tags, commands=commands))
@ -768,8 +799,10 @@ def create_aws_tasks():
tags = [*base_tags, f"auth-aws-{test_type}"]
if "t" in python:
tags.append("free-threaded")
name = get_task_name(f"{base_name}-{test_type}", python=python)
test_vars = dict(TEST_NAME="auth_aws", SUB_TEST_NAME=test_type, TOOLCHAIN_VERSION=python)
if python == ALL_PYTHONS[0] and test_type != "ecs":
test_vars["TEST_MIN_DEPS"] = "1"
name = get_task_name(f"{base_name}-{test_type}", **test_vars)
test_func = FunctionCall(func="run tests", vars=test_vars)
funcs = [server_func, assume_func, test_func]
tasks.append(EvgTask(name=name, tags=tags, commands=funcs))
@ -848,6 +881,8 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
TOOLCHAIN_VERSION=python,
VERSION=version,
)
if python == ALL_PYTHONS[0]:
vars["TEST_MIN_DEPS"] = "1"
test_func = FunctionCall(func="run tests", vars=vars)
tags = ["ocsp", f"ocsp-{algo}", version]
@ -856,16 +891,30 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
if algo == "valid-cert-server-staples" and version == "latest":
tags.append("pr")
task_name = get_task_name(
f"test-ocsp-{algo}-{base_task_name}",
python=python,
version=version,
)
task_name = get_task_name(f"test-ocsp-{algo}-{base_task_name}", **vars)
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
return tasks
def create_min_support_tasks():
server_func = FunctionCall(func="run server")
from generate_config_utils import MIN_SUPPORT_VERSIONS
tasks = []
for python, topology in product(MIN_SUPPORT_VERSIONS, TOPOLOGIES):
auth, ssl = get_standard_auth_ssl(topology)
vars = dict(UV_PYTHON=python, AUTH=auth, SSL=ssl, TOPOLOGY=topology)
test_func = FunctionCall(func="run tests", vars=vars)
task_name = get_task_name(
"test-min-support", python=python, topology=topology, auth=auth, ssl=ssl
)
tags = ["test-min-support"]
commands = [server_func, test_func]
tasks.append(EvgTask(name=task_name, tags=tags, commands=commands))
return tasks
def create_aws_lambda_tasks():
assume_func = FunctionCall(func="assume ec2 role")
vars = dict(TEST_NAME="aws_lambda")
@ -1140,6 +1189,14 @@ def create_run_tests_func():
return "run tests", [setup_cmd, test_cmd]
def create_test_numpy_func():
includes = ["TOOLCHAIN_VERSION"]
test_cmd = get_subprocess_exec(
include_expansions_in_env=includes, args=[".evergreen/just.sh", "test-numpy"]
)
return "test numpy", [test_cmd]
def create_cleanup_func():
cmd = get_subprocess_exec(args=[".evergreen/scripts/cleanup.sh"])
return "cleanup", [cmd]

View File

@ -24,6 +24,7 @@ from shrub.v3.shrub_service import ShrubService
ALL_VERSIONS = ["4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
CPYTHONS = ["3.10", "3.11", "3.12", "3.13", "3.14t", "3.14"]
PYPYS = ["pypy3.11"]
MIN_SUPPORT_VERSIONS = ["3.9", "pypy3.9", "pypy3.10"]
ALL_PYTHONS = CPYTHONS + PYPYS
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
BATCHTIME_WEEK = 10080
@ -42,7 +43,7 @@ DISPLAY_LOOKUP = dict(
sync={"sync": "Sync", "async": "Async"},
coverage={"1": "cov"},
no_ext={"1": "No C"},
test_min_deps={True: "Min Deps"},
test_min_deps={"1": "Min Deps"},
)
HOSTS = dict()
@ -63,7 +64,6 @@ HOSTS["macos"] = Host("macos", "macos-14", "macOS", dict())
HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64", dict())
HOSTS["ubuntu20"] = Host("ubuntu20", "ubuntu2004-small", "Ubuntu-20", dict())
HOSTS["ubuntu22"] = Host("ubuntu22", "ubuntu2204-small", "Ubuntu-22", dict())
HOSTS["rhel7"] = Host("rhel7", "rhel79-small", "RHEL7", dict())
HOSTS["perf"] = Host("perf", "rhel90-dbx-perf-large", "", dict())
HOSTS["debian11"] = Host("debian11", "debian11-small", "Debian11", dict())
DEFAULT_HOST = HOSTS["rhel8"]
@ -172,7 +172,7 @@ def get_common_name(base: str, sep: str, **kwargs) -> str:
display_name = f"{display_name}{sep}{version}"
for key, value in kwargs.items():
name = value
if key.lower() == "python":
if key.lower() in ["python", "toolchain_version"]:
if not value.startswith("pypy"):
name = f"Python{value}"
else:

View File

@ -6,7 +6,6 @@ import pathlib
import subprocess
from argparse import Namespace
from subprocess import CalledProcessError
from typing import Optional
def resync_specs(directory: pathlib.Path, errored: dict[str, str]) -> None:
@ -32,14 +31,27 @@ def resync_specs(directory: pathlib.Path, errored: dict[str, str]) -> None:
def apply_patches(errored):
print("Beginning to apply patches")
subprocess.run(["bash", "./.evergreen/remove-unimplemented-tests.sh"], check=True) # noqa: S603, S607
subprocess.run(
["bash", "./.evergreen/remove-unimplemented-tests.sh"], # noqa: S603, S607
check=True,
)
try:
subprocess.run(
["git apply -R --allow-empty --whitespace=fix ./.evergreen/spec-patch/*"], # noqa: S607
shell=True, # noqa: S602
check=True,
stderr=subprocess.PIPE,
)
# Avoid shell=True by passing arguments as a list.
# Note: glob expansion doesn't work in shell=False, so we use a list of files.
patches = [str(p) for p in pathlib.Path("./.evergreen/spec-patch/").glob("*")]
if patches:
subprocess.run(
[ # noqa: S603, S607
"git",
"apply",
"-R",
"--allow-empty",
"--whitespace=fix",
*patches,
],
check=True,
stderr=subprocess.PIPE,
)
except CalledProcessError as exc:
errored["applying patches"] = exc.stderr
@ -73,17 +85,24 @@ def check_new_spec_directories(directory: pathlib.Path) -> list[str]:
return list(spec_set - test_set)
def write_summary(errored: dict[str, str], new: list[str], filename: Optional[str]) -> None:
def write_summary(errored: dict[str, str], new: list[str], filename: str | None) -> None:
"""Generate the PR description"""
pr_body = ""
# Avoid shell=True and complex pipes by using Python to process git output
process = subprocess.run(
["git diff --name-only | awk -F'/' '{print $2}' | sort | uniq"], # noqa: S607
shell=True, # noqa: S602
["git", "diff", "--name-only"], # noqa: S603, S607
capture_output=True,
text=True,
check=True,
)
succeeded = process.stdout.strip().split()
changed_files = process.stdout.strip().splitlines()
succeeded_set = set()
for f in changed_files:
parts = f.split("/")
if len(parts) > 1:
succeeded_set.add(parts[1])
succeeded = sorted(succeeded_set)
if len(succeeded) > 0:
pr_body += "The following specs were changed:\n -"
pr_body += "\n -".join(succeeded)
@ -120,7 +139,9 @@ if __name__ == "__main__":
description="Python Script to resync all specs and generate summary for PR."
)
parser.add_argument(
"--filename", help="Name of file for the summary to be written into.", default=None
"--filename",
help="Name of file for the summary to be written into.",
default=None,
)
args = parser.parse_args()
main(args)

View File

@ -12,6 +12,7 @@ set -eu
# TEST_CRYPT_SHARED If non-empty, install crypt_shared lib.
# MONGODB_API_VERSION The mongodb api version to use in tests.
# MONGODB_URI If non-empty, use as the MONGODB_URI in tests.
# USE_ACTIVE_VENV If non-empty, use the active virtual environment.
SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
@ -21,5 +22,5 @@ if [ -f $SCRIPT_DIR/env.sh ]; then
fi
echo "Setting up tests with args \"$*\"..."
uv run $SCRIPT_DIR/setup_tests.py "$@"
uv run ${USE_ACTIVE_VENV:+--active} "$SCRIPT_DIR/setup_tests.py" "$@"
echo "Setting up tests with args \"$*\"... done."

View File

@ -1,12 +1,10 @@
from __future__ import annotations
import base64
import io
import os
import platform
import shutil
import stat
import tarfile
from pathlib import Path
from urllib import request
@ -117,9 +115,10 @@ def setup_libmongocrypt():
LOGGER.info(f"Fetching {url}...")
with request.urlopen(request.Request(url), timeout=15.0) as response: # noqa: S310
if response.status == 200:
fileobj = io.BytesIO(response.read())
with tarfile.open("libmongocrypt.tar.gz", fileobj=fileobj) as fid:
fid.extractall(Path.cwd() / "libmongocrypt")
with Path("libmongocrypt.tar.gz").open("wb") as f:
f.write(response.read())
Path("libmongocrypt").mkdir()
run_command("tar -xzf libmongocrypt.tar.gz -C libmongocrypt")
LOGGER.info(f"Fetching {url}... done.")
run_command("ls -la libmongocrypt")

View File

@ -0,0 +1,31 @@
diff --git a/test/discovery_and_monitoring/errors/error_handling_handshake.json b/test/discovery_and_monitoring/errors/error_handling_handshake.json
index 56ca7d113..bf83f46f6 100644
--- a/test/discovery_and_monitoring/errors/error_handling_handshake.json
+++ b/test/discovery_and_monitoring/errors/error_handling_handshake.json
@@ -97,14 +97,22 @@
"outcome": {
"servers": {
"a:27017": {
- "type": "Unknown",
- "topologyVersion": null,
+ "type": "RSPrimary",
+ "setName": "rs",
+ "topologyVersion": {
+ "processId": {
+ "$oid": "000000000000000000000001"
+ },
+ "counter": {
+ "$numberLong": "1"
+ }
+ },
"pool": {
- "generation": 1
+ "generation": 0
}
}
},
- "topologyType": "ReplicaSetNoPrimary",
+ "topologyType": "ReplicaSetWithPrimary",
"logicalSessionTimeoutMinutes": null,
"setName": "rs"
}

View File

@ -1,587 +0,0 @@
diff --git a/test/csot/command-execution.json b/test/csot/command-execution.json
index aa9c3eb2..212cd410 100644
--- a/test/csot/command-execution.json
+++ b/test/csot/command-execution.json
@@ -1,6 +1,6 @@
{
"description": "timeoutMS behaves correctly during command execution",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.4.7",
@@ -69,8 +69,10 @@
"appName": "reduceMaxTimeMSTest",
"w": 1,
"timeoutMS": 500,
- "heartbeatFrequencyMS": 500
+ "heartbeatFrequencyMS": 500,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"observeEvents": [
"commandStartedEvent"
]
@@ -185,8 +187,10 @@
"appName": "rttTooHighTest",
"w": 1,
"timeoutMS": 10,
- "heartbeatFrequencyMS": 500
+ "heartbeatFrequencyMS": 500,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"observeEvents": [
"commandStartedEvent"
]
@@ -316,8 +320,10 @@
"appName": "reduceMaxTimeMSTest",
"w": 1,
"timeoutMS": 90,
- "heartbeatFrequencyMS": 100000
+ "heartbeatFrequencyMS": 100000,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"observeEvents": [
"commandStartedEvent"
]
diff --git a/test/csot/convenient-transactions.json b/test/csot/convenient-transactions.json
index 3868b302..f9d03429 100644
--- a/test/csot/convenient-transactions.json
+++ b/test/csot/convenient-transactions.json
@@ -1,6 +1,6 @@
{
"description": "timeoutMS behaves correctly for the withTransaction API",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.4",
@@ -21,8 +21,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 500
+ "timeoutMS": 500,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
diff --git a/test/csot/error-transformations.json b/test/csot/error-transformations.json
index 4889e395..89be49f0 100644
--- a/test/csot/error-transformations.json
+++ b/test/csot/error-transformations.json
@@ -1,6 +1,6 @@
{
"description": "MaxTimeMSExpired server errors are transformed into a custom timeout error",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.0",
@@ -26,8 +26,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
diff --git a/test/csot/global-timeoutMS.json b/test/csot/global-timeoutMS.json
index f1edbe68..9d8046d1 100644
--- a/test/csot/global-timeoutMS.json
+++ b/test/csot/global-timeoutMS.json
@@ -1,6 +1,6 @@
{
"description": "timeoutMS can be configured on a MongoClient",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.4",
@@ -38,8 +38,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -217,8 +219,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -390,8 +394,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -569,8 +575,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -762,8 +770,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -941,8 +951,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -1120,8 +1132,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -1305,8 +1319,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -1484,8 +1500,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -1663,8 +1681,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -1842,8 +1862,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -2021,8 +2043,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -2194,8 +2218,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -2375,8 +2401,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -2554,8 +2582,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -2733,8 +2763,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -2906,8 +2938,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -3079,8 +3113,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -3258,8 +3294,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -3441,8 +3479,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -3628,8 +3668,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -3807,8 +3849,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -3986,8 +4030,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -4171,8 +4217,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -4360,8 +4408,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -4549,8 +4599,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -4728,8 +4780,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -4913,8 +4967,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -5102,8 +5158,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -5297,8 +5355,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -5482,8 +5542,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
@@ -5677,8 +5739,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 250
+ "timeoutMS": 250,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
diff --git a/test/csot/non-tailable-cursors.json b/test/csot/non-tailable-cursors.json
index 291c6e72..58c59cb3 100644
--- a/test/csot/non-tailable-cursors.json
+++ b/test/csot/non-tailable-cursors.json
@@ -1,6 +1,6 @@
{
"description": "timeoutMS behaves correctly for non-tailable cursors",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.4"
@@ -17,8 +17,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 200
+ "timeoutMS": 200,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
diff --git a/test/csot/retryability-timeoutMS.json b/test/csot/retryability-timeoutMS.json
index 9daad260..5a0c9f36 100644
--- a/test/csot/retryability-timeoutMS.json
+++ b/test/csot/retryability-timeoutMS.json
@@ -1,6 +1,6 @@
{
"description": "timeoutMS behaves correctly for retryable operations",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.0",
@@ -26,8 +26,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 100
+ "timeoutMS": 100,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent"
diff --git a/test/csot/runCursorCommand.json b/test/csot/runCursorCommand.json
index 36f774fb..e5182e33 100644
--- a/test/csot/runCursorCommand.json
+++ b/test/csot/runCursorCommand.json
@@ -1,6 +1,6 @@
{
"description": "runCursorCommand",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.4"
@@ -16,6 +16,10 @@
{
"client": {
"id": "commandClient",
+ "uriOptions": {
+ "minPoolSize": 1
+ },
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent",
diff --git a/test/csot/sessions-inherit-timeoutMS.json b/test/csot/sessions-inherit-timeoutMS.json
index 13ea91c7..dbf163e4 100644
--- a/test/csot/sessions-inherit-timeoutMS.json
+++ b/test/csot/sessions-inherit-timeoutMS.json
@@ -1,6 +1,6 @@
{
"description": "sessions inherit timeoutMS from their parent MongoClient",
- "schemaVersion": "1.9",
+ "schemaVersion": "1.26",
"runOnRequirements": [
{
"minServerVersion": "4.4",
@@ -21,8 +21,10 @@
"client": {
"id": "client",
"uriOptions": {
- "timeoutMS": 500
+ "timeoutMS": 500,
+ "minPoolSize": 1
},
+ "awaitMinPoolSizeMS": 10000,
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent",

View File

@ -1,38 +1,33 @@
<!-- Thanks for contributing! -->
<!-- Please ensure that the title of the PR is in the following form:
[Issue Type]-[Issue Key]: Issue Title
[JIRA TICKET]: Issue Title
If you are an external contributor and there is no JIRA ticket associated with your change, then use your best judgement
for the PR title. A MongoDB employee will create a JIRA ticket and edit the name and links as appropriate.
Note on AI Contributions:
We do not accept pull requests that are primarily or substantially generated by AI tools (ChatGPT, Copilot, etc.).
All contributions must be written and understood by human contributors.
-->
[Issue Key](https://jira.mongodb.org/browse/%7BISSUE_KEY%7D)
## Summary
<!-- What conceptually is this PR introducing? If context is already provided from the JIRA ticket, still place it in the
Pull Request as you should not make the reviewer do digging for a basic summary. -->
[JIRA TICKET]
## Changes in this PR
<!-- What changes did you make to the code? What new APIs (public or private) were added, removed, or edited to generate
the desired outcome explained in the above summary? -->
## Testing Plan
## Test Plan
<!-- How did you test the code? If you added unit tests, you can say that. If you didnt introduce unit tests, explain why.
All code should be tested in some way so please list what your validation strategy was. -->
### Screenshots (optional)
<!-- Usually a great supplement to a test plan, especially if this requires local testing. -->
## Checklist
<!-- Do not delete the items provided on this checklist. -->
### Checklist for Author
- [ ] Did you update the changelog (if necessary)?
- [ ] Is the intention of the code captured in relevant tests?
- [ ] If there are new TODOs, has a related JIRA ticket been created?
- [ ] Is there test coverage?
- [ ] Is any followup work tracked in a JIRA ticket? If so, add link(s).
### Checklist for Reviewer {@primary_reviewer}
### Checklist for Reviewer
- [ ] Does the title of the PR reference a JIRA Ticket?
- [ ] Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
- [ ] Have you checked for spelling & grammar errors?
- [ ] Is all relevant documentation (README or docstring) updated?
## Focus Areas for Reviewer (optional)
<!-- List any complex portion of code you believe needs additional scrutiny and explain why. -->

View File

@ -38,7 +38,7 @@ jobs:
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@ -63,6 +63,6 @@ jobs:
pip install -e .
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4
with:
category: "/language:${{matrix.language}}"

View File

@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout pymongo
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
@ -83,6 +83,7 @@ jobs:
- name: Assert all versions in wheelhouse
if: ${{ ! startsWith(matrix.buildplat[1], 'macos') }}
run: |
ls wheelhouse/*cp39*.whl
ls wheelhouse/*cp310*.whl
ls wheelhouse/*cp311*.whl
ls wheelhouse/*cp312*.whl
@ -91,7 +92,7 @@ jobs:
# Free-threading builds:
ls wheelhouse/*cp314t*.whl
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: wheel-${{ matrix.buildplat[1] }}
path: ./wheelhouse/*.whl
@ -101,7 +102,7 @@ jobs:
name: Make SDist
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
@ -110,7 +111,7 @@ jobs:
- uses: actions/setup-python@v6
with:
# Build sdist on lowest supported Python
python-version: "3.10"
python-version: "3.9"
- name: Build SDist
run: |
@ -124,7 +125,7 @@ jobs:
cd ..
python -c "from pymongo import has_c; assert has_c()"
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: "sdist"
path: ./dist/*.tar.gz
@ -135,13 +136,13 @@ jobs:
name: Download Wheels
steps:
- name: Download all workflow run artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
- name: Flatten directory
working-directory: .
run: |
find . -mindepth 2 -type f -exec mv {} . \;
find . -type d -empty -delete
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: all-dist-${{ github.run_id }}
path: "./*"

View File

@ -75,7 +75,7 @@ jobs:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: all-dist-${{ github.run_id }}
path: dist/

View File

@ -1,6 +1,6 @@
name: Generate SBOM
# This workflow uses cdxgen and publishes an sbom.json artifact.
# This workflow uses cyclonedx-py and publishes an sbom.json artifact.
# It runs on manual trigger or when package files change on main branch,
# and creates a PR with the updated SBOM.
# Internal documentation: go/sbom-scope
@ -10,8 +10,10 @@ on:
push:
branches: ['master']
paths:
- 'pyproject.toml'
- 'requirements.txt'
- 'requirements/**.txt'
- '!requirements/docs.txt'
- '!requirements/test.txt'
permissions:
contents: write
@ -27,12 +29,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.10"
@ -40,27 +42,45 @@ jobs:
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python tools/generate_sbom_requirements.py
pip install -r sbom-requirements.txt
pip install .
npx @cyclonedx/cdxgen -t python --exclude "uv.lock" --exclude "requirements/**" --exclude "requirements.txt" --spec-version 1.5 --no-validate --json-pretty -o sbom.json
env:
FETCH_LICENSE: true
pip uninstall -y pip setuptools
deactivate
python -m venv .venv-sbom
source .venv-sbom/bin/activate
pip install cyclonedx-bom==7.2.1
cyclonedx-py environment --spec-version 1.5 --output-format JSON --output-file sbom.json .venv
# Add PURL for pymongo (local package doesn't get PURL automatically)
jq '(.components[] | select(.name == "pymongo" and .purl == null)) |= (. + {purl: ("pkg:pypi/pymongo@" + .version)})' sbom.json > sbom.tmp.json && mv sbom.tmp.json sbom.json
- name: Download CycloneDX CLI
run: |
curl -L -s -o /tmp/cyclonedx "https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.29.1/cyclonedx-linux-x64"
chmod +x /tmp/cyclonedx
- name: Validate SBOM
run: /tmp/cyclonedx validate --input-file sbom.json --fail-on-errors
- name: Cleanup
if: always()
run: rm -rf .venv .venv-sbom sbom-requirements.txt
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: sbom
path: sbom.json
if-no-files-found: error
- name: Create Pull Request
uses: peter-evans/create-pull-request@b4733b9419fd47bbfa1807b15627e17cd70b5b22
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: Update SBOM after dependency changes'
branch: auto-update-sbom-${{ github.run_id }}
delete-branch: true
title: 'chore: Update SBOM'
title: 'Automation: Update SBOM'
body: |
## Automated SBOM Update
@ -70,7 +90,7 @@ jobs:
- Updated `sbom.json` to reflect current dependencies
### Verification
The SBOM was generated using cdxgen with the current Python environment.
The SBOM was generated using cyclonedx-py v7.2.1 with the current Python environment.
### Triggered by
- Commit: ${{ github.sha }}
@ -82,7 +102,3 @@ jobs:
sbom
automated
dependencies
- name: Cleanup
if: always()
run: rm -rf .venv

View File

@ -22,11 +22,11 @@ jobs:
static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: "3.10"
@ -64,11 +64,11 @@ jobs:
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
@ -83,11 +83,11 @@ jobs:
runs-on: ubuntu-latest
name: DocTest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: "3.10"
@ -108,11 +108,11 @@ jobs:
name: Docs Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: "3.10"
@ -127,11 +127,11 @@ jobs:
name: Link Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: "3.10"
@ -149,11 +149,11 @@ jobs:
matrix:
python: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: "${{matrix.python}}"
@ -170,11 +170,11 @@ jobs:
runs-on: ubuntu-latest
name: Integration Tests
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
enable-cache: true
python-version: "3.10"
@ -200,7 +200,7 @@ jobs:
runs-on: ubuntu-latest
name: "Make an sdist"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
@ -208,13 +208,13 @@ jobs:
cache: 'pip'
cache-dependency-path: 'pyproject.toml'
# Build sdist on lowest supported Python
python-version: "3.10"
python-version: "3.9"
- name: Build SDist
shell: bash
run: |
pip install build
python -m build --sdist
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v6
with:
name: "sdist"
path: dist/*.tar.gz
@ -226,7 +226,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Download sdist
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
path: sdist/
- name: Unpack SDist
@ -242,7 +242,7 @@ jobs:
cache: 'pip'
cache-dependency-path: 'sdist/test/pyproject.toml'
# Test sdist on lowest supported Python
python-version: "3.10"
python-version: "3.9"
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
- name: Run connect test from sdist
@ -260,13 +260,13 @@ jobs:
runs-on: ubuntu-latest
name: Test minimum dependencies and Python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
with:
python-version: "3.10"
python-version: "3.9"
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
with:

View File

@ -14,8 +14,8 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@1aba86d8e1245be7a9ca003d46fcc85a76e6aa61
uses: zizmorcore/zizmor-action@706c51b5bce7adb027de71ab36d865f5d3fcc7b7

View File

@ -16,7 +16,7 @@ be of interest or that has already been addressed.
## Supported Interpreters
PyMongo supports CPython 3.10+ and PyPy3.10+. Language features not
PyMongo supports CPython 3.9+ and PyPy3.9+. Language features not
supported by all interpreters can not be used.
## Style Guide
@ -387,6 +387,11 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
To run any of the test suites with minimum supported dependencies, pass `--test-min-deps` to
`just setup-tests`.
## Testing time-dependent operations
- `test.utils_shared.delay` - One can trigger an arbitrarily long-running operation on the server using this delay utility
in combination with a `$where` operation. Use this to test behaviors around timeouts or signals.
## Adding a new test suite
- If adding new tests files that should only be run for that test suite, add a pytest marker to the file and add
@ -482,6 +487,7 @@ results into the patch file.
For example: the imaginary, unimplemented PYTHON-1234 ticket has associated spec test changes. To add those changes to `PYTHON-1234.patch`), do the following:
```bash
git diff HEAD~1 path/to/file >> .evergreen/spec-patch/PYTHON-1234.patch
```
#### Running Locally
Both `resync-all-specs.sh` and `resync-all-specs.py` can be run locally (and won't generate a PR).
@ -519,8 +525,10 @@ Use this generated file as a starting point for the completed conversion.
The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`
## Generating a flame graph using py-spy
## CPU profiling
To profile a test script and generate a flame graph, follow these steps:
1. Install `py-spy` if you haven't already:
```bash
pip install py-spy
@ -530,6 +538,26 @@ To profile a test script and generate a flame graph, follow these steps:
(Note: on macOS you will need to run this command using `sudo` to allow `py-spy` to attach to the Python process.)
4. If you need to include native code (for example the C extensions), profiling should be done on a Linux system, as macOS and Windows do not support the `--native` option of `py-spy`.
Creating an ubuntu Evergreen spawn host and using `scp` to copy the flamegraph `.svg` file back to your local machine is the best way to do this.
5. You can then view the flamegraph using an SVG viewer like a browser.
## Memory profiling
To test for a memory leak or any memory-related issues, the current best tool is [memray](https://bloomberg.github.io/memray/overview.html).
In order to include code from our C extensions, it must be run in native mode, on Linux.
To do so, either spin up an Ubuntu docker container or an Ubuntu Evergreen spawn host.
From the spawn host or Ubuntu image, do the following:
1. Install `memray` if you haven't already:
```bash
pip install memray
```
2. Inside your test script, perform any required setup and then loop over the code you want to profile for improved sampling.
3. Run memray with the script under test with the `--native` flag, e.g. `python -m memray run --native -o test.bin <path/to/script>`.
4. Generate the flamegraph with `python -m memray flamegraph -o test.html test.bin`.
See the [docs](https://bloomberg.github.io/memray/flamegraph.html) for more options.
5. Then, from the host computer, use either scp or docker cp to copy the flamegraph, e.g. `scp ubuntu@ec2-3-82-52-49.compute-1.amazonaws.com:/home/ubuntu/test.html .`.
6. You can then view the flamegraph html in a browser.
## Dependabot updates

View File

@ -97,7 +97,7 @@ package that is incompatible with PyMongo.
## Dependencies
PyMongo supports CPython 3.10+ and PyPy3.10+.
PyMongo supports CPython 3.9+ and PyPy3.9+.
Required dependencies:

View File

@ -65,6 +65,9 @@ if TYPE_CHECKING:
from array import array as _array
from mmap import mmap as _mmap
import numpy as np
import numpy.typing as npt
class UuidRepresentation:
UNSPECIFIED = 0
@ -234,13 +237,20 @@ class BinaryVector:
__slots__ = ("data", "dtype", "padding")
def __init__(self, data: Sequence[float | int], dtype: BinaryVectorDtype, padding: int = 0):
def __init__(
self,
data: Union[Sequence[float | int], npt.NDArray[np.number]],
dtype: BinaryVectorDtype,
padding: int = 0,
):
"""
:param data: Sequence of numbers representing the mathematical vector.
:param dtype: The data type stored in binary
:param padding: The number of bits in the final byte that are to be ignored
when a vector element's size is less than a byte
and the length of the vector is not a multiple of 8.
(Padding is equivalent to a negative value of `count` in
`numpy.unpackbits <https://numpy.org/doc/stable/reference/generated/numpy.unpackbits.html>`_)
"""
self.data = data
self.dtype = dtype
@ -425,9 +435,19 @@ class Binary(bytes):
...
@classmethod
@overload
def from_vector(
cls: Type[Binary],
vector: Union[BinaryVector, list[int], list[float]],
vector: npt.NDArray[np.number],
dtype: BinaryVectorDtype,
padding: int = 0,
) -> Binary:
...
@classmethod
def from_vector(
cls: Type[Binary],
vector: Union[BinaryVector, list[int], list[float], npt.NDArray[np.number]],
dtype: Optional[BinaryVectorDtype] = None,
padding: Optional[int] = None,
) -> Binary:
@ -459,34 +479,72 @@ class Binary(bytes):
vector = vector.data # type: ignore
padding = 0 if padding is None else padding
if dtype == BinaryVectorDtype.INT8: # pack ints in [-128, 127] as signed int8
format_str = "b"
if padding:
raise ValueError(f"padding does not apply to {dtype=}")
elif dtype == BinaryVectorDtype.PACKED_BIT: # pack ints in [0, 255] as unsigned uint8
format_str = "B"
if 0 <= padding > 7:
raise ValueError(f"{padding=}. It must be in [0,1, ..7].")
if padding and not vector:
raise ValueError("Empty vector with non-zero padding.")
elif dtype == BinaryVectorDtype.FLOAT32: # pack floats as float32
format_str = "f"
if padding:
raise ValueError(f"padding does not apply to {dtype=}")
else:
raise NotImplementedError("%s not yet supported" % dtype)
if not isinstance(dtype, BinaryVectorDtype):
raise TypeError(
"dtype must be a bson.BinaryVectorDtype of BinaryVectorDType.INT8, PACKED_BIT, FLOAT32"
)
metadata = struct.pack("<sB", dtype.value, padding)
data = struct.pack(f"<{len(vector)}{format_str}", *vector) # type: ignore
if isinstance(vector, list):
if dtype == BinaryVectorDtype.INT8: # pack ints in [-128, 127] as signed int8
format_str = "b"
if padding:
raise ValueError(f"padding does not apply to {dtype=}")
elif dtype == BinaryVectorDtype.PACKED_BIT: # pack ints in [0, 255] as unsigned uint8
format_str = "B"
if 0 <= padding > 7:
raise ValueError(f"{padding=}. It must be in [0,1, ..7].")
if padding and not vector:
raise ValueError("Empty vector with non-zero padding.")
elif dtype == BinaryVectorDtype.FLOAT32: # pack floats as float32
format_str = "f"
if padding:
raise ValueError(f"padding does not apply to {dtype=}")
else:
raise NotImplementedError("%s not yet supported" % dtype)
data = struct.pack(f"<{len(vector)}{format_str}", *vector)
else: # vector is numpy array or incorrect type.
try:
import numpy as np
except ImportError as exc:
raise ImportError(
"Failed to create binary from vector. Check type. If numpy array, numpy must be installed."
) from exc
if not isinstance(vector, np.ndarray):
raise TypeError(
"Could not create Binary. Vector must be a BinaryVector, list[int], list[float] or numpy ndarray."
)
if vector.ndim != 1:
raise ValueError(
"from_numpy_vector only supports 1D arrays as it creates a single vector."
)
if dtype == BinaryVectorDtype.FLOAT32:
vector = vector.astype(np.dtype("float32"), copy=False)
elif dtype == BinaryVectorDtype.INT8:
if vector.min() >= -128 and vector.max() <= 127:
vector = vector.astype(np.dtype("int8"), copy=False)
else:
raise ValueError("Values found outside INT8 range.")
elif dtype == BinaryVectorDtype.PACKED_BIT:
if vector.min() >= 0 and vector.max() <= 127:
vector = vector.astype(np.dtype("uint8"), copy=False)
else:
raise ValueError("Values found outside UINT8 range.")
else:
raise NotImplementedError("%s not yet supported" % dtype)
data = vector.tobytes()
if padding and len(vector) and not (data[-1] & ((1 << padding) - 1)) == 0:
raise ValueError(
"Vector has a padding P, but bits in the final byte lower than P are non-zero. They must be zero."
)
return cls(metadata + data, subtype=VECTOR_SUBTYPE)
def as_vector(self) -> BinaryVector:
"""From the Binary, create a list of numbers, along with dtype and padding.
def as_vector(self, return_numpy: bool = False) -> BinaryVector:
"""From the Binary, create a list or 1-d numpy array of numbers, along with dtype and padding.
:param return_numpy: If True, BinaryVector.data will be a one-dimensional numpy array. By default, it is a list.
:return: BinaryVector
.. versionadded:: 4.10
@ -495,54 +553,84 @@ class Binary(bytes):
if self.subtype != VECTOR_SUBTYPE:
raise ValueError(f"Cannot decode subtype {self.subtype} as a vector")
position = 0
dtype, padding = struct.unpack_from("<sB", self, position)
position += 2
dtype, padding = struct.unpack_from("<sB", self)
dtype = BinaryVectorDtype(dtype)
n_values = len(self) - position
offset = 2
n_bytes = len(self) - offset
if padding and dtype != BinaryVectorDtype.PACKED_BIT:
raise ValueError(
f"Corrupt data. Padding ({padding}) must be 0 for all but PACKED_BIT dtypes. ({dtype=})"
)
if dtype == BinaryVectorDtype.INT8:
dtype_format = "b"
format_string = f"<{n_values}{dtype_format}"
vector = list(struct.unpack_from(format_string, self, position))
return BinaryVector(vector, dtype, padding)
if not return_numpy:
if dtype == BinaryVectorDtype.INT8:
dtype_format = "b"
format_string = f"<{n_bytes}{dtype_format}"
vector = list(struct.unpack_from(format_string, self, offset))
return BinaryVector(vector, dtype, padding)
elif dtype == BinaryVectorDtype.FLOAT32:
n_bytes = len(self) - position
n_values = n_bytes // 4
if n_bytes % 4:
raise ValueError(
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
)
dtype_format = "f"
format_string = f"<{n_values}{dtype_format}"
vector = list(struct.unpack_from(format_string, self, position))
return BinaryVector(vector, dtype, padding)
elif dtype == BinaryVectorDtype.FLOAT32:
n_values = n_bytes // 4
if n_bytes % 4:
raise ValueError(
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
)
dtype_format = "f"
format_string = f"<{n_values}{dtype_format}"
vector = list(struct.unpack_from(format_string, self, offset))
return BinaryVector(vector, dtype, padding)
elif dtype == BinaryVectorDtype.PACKED_BIT:
# data packed as uint8
if padding and not n_values:
raise ValueError("Corrupt data. Vector has a padding P, but no data.")
if padding > 7 or padding < 0:
raise ValueError(f"Corrupt data. Padding ({padding}) must be between 0 and 7.")
dtype_format = "B"
format_string = f"<{n_values}{dtype_format}"
unpacked_uint8s = list(struct.unpack_from(format_string, self, position))
if padding and n_values and unpacked_uint8s[-1] & (1 << padding) - 1 != 0:
warnings.warn(
"Vector has a padding P, but bits in the final byte lower than P are non-zero. For pymongo>=5.0, they must be zero.",
DeprecationWarning,
stacklevel=2,
)
return BinaryVector(unpacked_uint8s, dtype, padding)
elif dtype == BinaryVectorDtype.PACKED_BIT:
# data packed as uint8
if padding and not n_bytes:
raise ValueError("Corrupt data. Vector has a padding P, but no data.")
if padding > 7 or padding < 0:
raise ValueError(f"Corrupt data. Padding ({padding}) must be between 0 and 7.")
dtype_format = "B"
format_string = f"<{n_bytes}{dtype_format}"
unpacked_uint8s = list(struct.unpack_from(format_string, self, offset))
if padding and n_bytes and unpacked_uint8s[-1] & (1 << padding) - 1 != 0:
warnings.warn(
"Vector has a padding P, but bits in the final byte lower than P are non-zero. For pymongo>=5.0, they must be zero.",
DeprecationWarning,
stacklevel=2,
)
return BinaryVector(unpacked_uint8s, dtype, padding)
else:
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
else:
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
else: # create a numpy array
try:
import numpy as np
except ImportError as exc:
raise ImportError(
"Converting binary to numpy.ndarray requires numpy to be installed."
) from exc
if dtype == BinaryVectorDtype.INT8:
data = np.frombuffer(self[offset:], dtype="int8")
elif dtype == BinaryVectorDtype.FLOAT32:
if n_bytes % 4:
raise ValueError(
"Corrupt data. N bytes for a float32 vector must be a multiple of 4."
)
data = np.frombuffer(self[offset:], dtype="float32")
elif dtype == BinaryVectorDtype.PACKED_BIT:
# data packed as uint8
if padding and not n_bytes:
raise ValueError("Corrupt data. Vector has a padding P, but no data.")
if padding > 7 or padding < 0:
raise ValueError(f"Corrupt data. Padding ({padding}) must be between 0 and 7.")
data = np.frombuffer(self[offset:], dtype="uint8")
if padding and np.unpackbits(data[-1])[-padding:].sum() > 0:
warnings.warn(
"Vector has a padding P, but bits in the final byte lower than P are non-zero. For pymongo>=5.0, they must be zero.",
DeprecationWarning,
stacklevel=2,
)
else:
raise NotImplementedError("Binary Vector dtype %s not yet supported" % dtype.name)
return BinaryVector(data, dtype, padding)
@property
def subtype(self) -> int:

View File

@ -273,9 +273,6 @@ if TYPE_CHECKING:
def _arguments_repr(self) -> str:
...
def _options_dict(self) -> dict[Any, Any]:
...
# NamedTuple API
@classmethod
def _make(cls, obj: Iterable[Any]) -> CodecOptions[_DocumentType]:
@ -466,19 +463,6 @@ else:
)
)
def _options_dict(self) -> dict[str, Any]:
"""Dictionary of the arguments used to create this object."""
# TODO: PYTHON-2442 use _asdict() instead
return {
"document_class": self.document_class,
"tz_aware": self.tz_aware,
"uuid_representation": self.uuid_representation,
"unicode_decode_error_handler": self.unicode_decode_error_handler,
"tzinfo": self.tzinfo,
"type_registry": self.type_registry,
"datetime_conversion": self.datetime_conversion,
}
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._arguments_repr()})"
@ -494,7 +478,7 @@ else:
.. versionadded:: 3.5
"""
opts = self._options_dict()
opts = self._asdict()
opts.update(kwargs)
return CodecOptions(**opts)

View File

@ -382,19 +382,6 @@ class JSONOptions(_BASE_CLASS):
)
)
def _options_dict(self) -> dict[Any, Any]:
# TODO: PYTHON-2442 use _asdict() instead
options_dict = super()._options_dict()
options_dict.update(
{
"strict_number_long": self.strict_number_long,
"datetime_representation": self.datetime_representation,
"strict_uuid": self.strict_uuid,
"json_mode": self.json_mode,
}
)
return options_dict
def with_options(self, **kwargs: Any) -> JSONOptions:
"""
Make a copy of this JSONOptions, overriding some options::
@ -408,7 +395,7 @@ class JSONOptions(_BASE_CLASS):
.. versionadded:: 3.12
"""
opts = self._options_dict()
opts = self._asdict()
for opt in ("strict_number_long", "datetime_representation", "strict_uuid", "json_mode"):
opts[opt] = kwargs.get(opt, getattr(self, opt))
opts.update(kwargs)

View File

@ -15,7 +15,6 @@
"""Tools for working with MongoDB ObjectIds."""
from __future__ import annotations
import binascii
import datetime
import os
import struct
@ -98,11 +97,27 @@ class ObjectId:
objectid.rst>`_.
"""
if oid is None:
self.__generate()
# Generate a new value for this ObjectId.
with ObjectId._inc_lock:
inc = ObjectId._inc
ObjectId._inc = (inc + 1) % (_MAX_COUNTER_VALUE + 1)
# 4 bytes current time, 5 bytes random, 3 bytes inc.
self.__id = _PACK_INT_RANDOM(int(time.time()), ObjectId._random()) + _PACK_INT(inc)[1:4]
elif isinstance(oid, bytes) and len(oid) == 12:
self.__id = oid
elif isinstance(oid, str):
if len(oid) == 24:
try:
self.__id = bytes.fromhex(oid)
except (TypeError, ValueError):
_raise_invalid_id(oid)
else:
_raise_invalid_id(oid)
elif isinstance(oid, ObjectId):
self.__id = oid.binary
else:
self.__validate(oid)
raise TypeError(f"id must be an instance of (bytes, str, ObjectId), not {type(oid)}")
@classmethod
def from_datetime(cls: Type[ObjectId], generation_time: datetime.datetime) -> ObjectId:
@ -163,37 +178,6 @@ class ObjectId:
cls.__random = _random_bytes()
return cls.__random
def __generate(self) -> None:
"""Generate a new value for this ObjectId."""
with ObjectId._inc_lock:
inc = ObjectId._inc
ObjectId._inc = (inc + 1) % (_MAX_COUNTER_VALUE + 1)
# 4 bytes current time, 5 bytes random, 3 bytes inc.
self.__id = _PACK_INT_RANDOM(int(time.time()), ObjectId._random()) + _PACK_INT(inc)[1:4]
def __validate(self, oid: Any) -> None:
"""Validate and use the given id for this ObjectId.
Raises TypeError if id is not an instance of :class:`str`,
:class:`bytes`, or ObjectId. Raises InvalidId if it is not a
valid ObjectId.
:param oid: a valid ObjectId
"""
if isinstance(oid, ObjectId):
self.__id = oid.binary
elif isinstance(oid, str):
if len(oid) == 24:
try:
self.__id = bytes.fromhex(oid)
except (TypeError, ValueError):
_raise_invalid_id(oid)
else:
_raise_invalid_id(oid)
else:
raise TypeError(f"id must be an instance of (bytes, str, ObjectId), not {type(oid)}")
@property
def binary(self) -> bytes:
"""12-byte binary representation of this ObjectId."""
@ -234,7 +218,7 @@ class ObjectId:
self.__id = oid
def __str__(self) -> str:
return binascii.hexlify(self.__id).decode()
return self.__id.hex()
def __repr__(self) -> str:
return f"ObjectId('{self!s}')"

View File

@ -1,22 +1,32 @@
Changelog
=========
Changes in Version 4.16.0 (XXXX/XX/XX)
Changes in Version 4.16.0 (2026/01/07)
--------------------------------------
PyMongo 4.16 brings a number of changes including:
.. warning:: PyMongo 4.16 drops support for Python 3.9 and PyPy 3.10: Python 3.10+ or PyPy 3.11+ is now required.
- Dropped support for Python 3.9 and PyPy 3.10.
- Removed invalid documents from :class:`bson.errors.InvalidDocument` error messages as
doing so may leak sensitive user data.
Instead, invalid documents are stored in :attr:`bson.errors.InvalidDocument.document`.
- PyMongo now requires ``dnspython>=2.6.1``, since ``dnspython`` 1.0 is no longer maintained and is incompatible with
Python 3.10+. The minimum version is ``2.6.1`` to account for `CVE-2023-29483 <https://www.cve.org/CVERecord?id=CVE-2023-29483>`_.
- PyMongo now requires ``dnspython>=2.6.1``, since ``dnspython`` 1.0 is no longer maintained.
The minimum version is ``2.6.1`` to account for `CVE-2023-29483 <https://www.cve.org/CVERecord?id=CVE-2023-29483>`_.
- Removed support for Eventlet.
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
- Use Zstandard support from the standard library for Python 3.14+, and use ``backports.zstd`` for older versions.
- Fixed return type annotation for ``find_one_and_*`` methods on :class:`~pymongo.asynchronous.collection.AsyncCollection`
and :class:`~pymongo.synchronous.collection.Collection` to include ``None``.
- Added support for NumPy 1D-arrays in :class:`bson.binary.BinaryVector`.
- Prevented :class:`~pymongo.encryption.ClientEncryption` from loading the crypt
shared library to fix "MongoCryptError: An existing crypt_shared library is
loaded by the application" unless the linked library search path is set.
Changes in Version 4.15.5 (2025/12/02)
--------------------------------------
Version 4.15.5 is a bug fix release.
- Fixed a bug that could cause ``AutoReconnect("connection pool paused")`` errors when cursors fetched more documents from the database after SDAM heartbeat failures.
Changes in Version 4.15.4 (2025/10/21)
--------------------------------------

View File

@ -88,6 +88,8 @@ pygments_style = "sphinx"
linkcheck_ignore = [
"https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-monitoring.md#requesting-an-immediate-check",
"https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback",
"https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.md",
"https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.md",
"https://github.com/mongodb/libmongocrypt/blob/master/bindings/python/README.rst#installing-from-source",
r"https://wiki.centos.org/[\w/]*",
r"https://sourceforge.net/",
@ -186,8 +188,8 @@ latex_documents = [
("index", "PyMongo.tex", "PyMongo Documentation", "Michael Dirolf", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# The name of an image file (relative to this directory) to place at the top
# of the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,

View File

@ -107,3 +107,4 @@ The following is a list of people who have contributed to
- Jeffrey A. Clark (aclark4life)
- Steven Silvester (blink1073)
- Noah Stapp (NoahStapp)
- Cal Jacobson (cj81499)

View File

@ -46,6 +46,7 @@ from pymongo.asynchronous.client_session import AsyncClientSession
from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.cursor import AsyncCursor
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
from pymongo.common import validate_string
from pymongo.errors import (
BulkWriteError,

View File

@ -57,6 +57,7 @@ from pymongo.synchronous.client_session import ClientSession
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.cursor import Cursor
from pymongo.synchronous.database import Database
from pymongo.synchronous.helpers import next
_IS_SYNC = True

View File

@ -4,7 +4,7 @@ set shell := ["bash", "-c"]
export UV_FROZEN := "1"
# Commonly used command segments.
typing_run := "uv run --group typing --extra aws --extra encryption --extra ocsp --extra snappy --extra test --extra zstd"
typing_run := "uv run --group typing --extra aws --extra encryption --with numpy --extra ocsp --extra snappy --extra test --extra zstd"
docs_run := "uv run --extra docs"
doc_build := "./doc/_build"
mypy_args := "--install-types --non-interactive"
@ -40,14 +40,14 @@ typing: && resync
[group('typing')]
typing-mypy: && resync
{{typing_run}} mypy {{mypy_args}} bson gridfs tools pymongo
{{typing_run}} mypy {{mypy_args}} --config-file mypy_test.ini test
{{typing_run}} mypy {{mypy_args}} test/test_typing.py test/test_typing_strict.py
{{typing_run}} python -m mypy {{mypy_args}} bson gridfs tools pymongo
{{typing_run}} python -m mypy {{mypy_args}} --config-file mypy_test.ini test
{{typing_run}} python -m mypy {{mypy_args}} test/test_typing.py test/test_typing_strict.py
[group('typing')]
typing-pyright: && resync
{{typing_run}} pyright test/test_typing.py test/test_typing_strict.py
{{typing_run}} pyright -p strict_pyrightconfig.json test/test_typing_strict.py
{{typing_run}} python -m pyright test/test_typing.py test/test_typing_strict.py
{{typing_run}} python -m pyright -p strict_pyrightconfig.json test/test_typing_strict.py
[group('lint')]
lint: && resync
@ -59,7 +59,11 @@ lint-manual: && resync
[group('test')]
test *args="-v --durations=5 --maxfail=10": && resync
uv run --extra test pytest {{args}}
uv run --extra test python -m pytest {{args}}
[group('test')]
test-numpy: && resync
uv run --extra test --with numpy python -m pytest test/test_bson.py
[group('test')]
run-tests *args: && resync

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import re
from typing import List, Tuple, Union
__version__ = "4.16.0.dev0"
__version__ = "4.17.0.dev0"
def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]:

View File

@ -3314,7 +3314,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and deletes it, returning the document.
>>> await db.test.count_documents({'x': 1})
@ -3324,6 +3324,10 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
>>> await db.test.count_documents({'x': 1})
1
Returns ``None`` if no document matches the filter.
>>> await db.test.find_one_and_delete({'_exists': False})
If multiple documents match *filter*, a *sort* can be applied.
>>> async for doc in db.test.find({'x': 1}):
@ -3406,10 +3410,22 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and replaces it, returning either the
original or the replaced document.
>>> await db.test.find_one({'x': 1})
{'_id': 0, 'x': 1}
>>> await db.test.find_one_and_replace({'x': 1}, {'y': 2})
{'_id': 0, 'x': 1}
>>> await db.test.find_one({'x': 1})
>>> await db.test.find_one({'y': 2})
{'_id': 0, 'y': 2}
Returns ``None`` if no document matches the filter.
>>> await db.test.find_one_and_replace({'_exists': False}, {'x': 1})
The :meth:`find_one_and_replace` method differs from
:meth:`find_one_and_update` by replacing the document matched by
*filter*, rather than modifying the existing document.
@ -3514,13 +3530,17 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and updates it, returning either the
original or the updated document.
>>> await db.test.find_one({'_id': 665})
{'_id': 665, 'done': False, 'count': 25}
>>> await db.test.find_one_and_update(
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
{'_id': 665, 'done': False, 'count': 25}}
{'_id': 665, 'done': False, 'count': 25}
>>> await db.test.find_one({'_id': 665})
{'_id': 665, 'done': True, 'count': 26}
Returns ``None`` if no document matches the filter.

View File

@ -37,6 +37,7 @@ from bson import RE_TYPE, _convert_raw_document_lists_to_streams
from bson.code import Code
from bson.son import SON
from pymongo import _csot, helpers_shared
from pymongo.asynchronous.helpers import anext
from pymongo.collation import validate_collation_or_none
from pymongo.common import (
validate_is_document_type,

View File

@ -701,7 +701,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
.. versionadded:: 3.9
.. _aggregation pipeline:
https://mongodb.com/docs/manual/reference/operator/aggregation-pipeline
https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
.. _aggregate command:
https://mongodb.com/docs/manual/reference/command/aggregate

View File

@ -717,7 +717,10 @@ class AsyncClientEncryption(Generic[_DocumentType]):
self._encryption = AsyncExplicitEncrypter(
self._io_callbacks,
_create_mongocrypt_options(
kms_providers=kms_providers, schema_map=None, key_expiration_ms=key_expiration_ms
kms_providers=kms_providers,
schema_map=None,
key_expiration_ms=key_expiration_ms,
bypass_encryption=True, # Don't load crypt_shared
),
)
# Use the same key vault collection as the callback.

View File

@ -16,9 +16,11 @@
from __future__ import annotations
import asyncio
import builtins
import functools
import random
import socket
import sys
import time as time # noqa: PLC0414 # needed in sync version
from typing import (
Any,
@ -208,3 +210,17 @@ async def _getaddrinfo(
return await loop.getaddrinfo(host, port, **kwargs) # type: ignore[return-value]
else:
return socket.getaddrinfo(host, port, **kwargs)
if sys.version_info >= (3, 10):
anext = builtins.anext
aiter = builtins.aiter
else:
async def anext(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
return await cls.__anext__()
def aiter(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#anext."""
return cls.__aiter__()

View File

@ -2847,7 +2847,7 @@ class _ClientConnectionRetryable(Generic[T]):
if self._last_error is None:
self._last_error = exc
if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded:
if self._server is not None:
self._deprioritized_servers.append(self._server)
self._always_retryable = always_retryable

View File

@ -108,21 +108,6 @@ if TYPE_CHECKING:
from pymongo.typings import _Address, _CollationIn
from pymongo.write_concern import WriteConcern
try:
from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl
def _set_non_inheritable_non_atomic(fd: int) -> None:
"""Set the close-on-exec flag on the given file descriptor."""
flags = fcntl(fd, F_GETFD)
fcntl(fd, F_SETFD, flags | FD_CLOEXEC)
except ImportError:
# Windows, various platforms we don't claim to support
# (Jython, IronPython, ..), systems that don't provide
# everything we need from fcntl, etc.
def _set_non_inheritable_non_atomic(fd: int) -> None: # noqa: ARG001
"""Dummy function for platforms that don't provide fcntl."""
_IS_SYNC = False
@ -710,8 +695,6 @@ class PoolState:
CLOSED = 3
# Do *not* explicitly inherit from object or Jython won't call __del__
# https://bugs.jython.org/issue1057
class Pool:
def __init__(
self,

View File

@ -111,7 +111,7 @@ class Topology:
self._publish_tp = self._listeners is not None and self._listeners.enabled_for_topology
# Create events queue if there are publishers.
self._events = None
self._events: queue.Queue[Any] | None = None
self.__events_executor: Any = None
if self._publish_server or self._publish_tp:
@ -126,6 +126,7 @@ class Topology:
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._events.put((self._listeners.publish_topology_opened, (self._topology_id,)))
self._settings = topology_settings
topology_description = TopologyDescription(
@ -143,6 +144,7 @@ class Topology:
)
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_topology_description_changed,
@ -161,6 +163,7 @@ class Topology:
for seed in topology_settings.seeds:
if self._publish_server:
assert self._events is not None
assert self._listeners is not None
self._events.put((self._listeners.publish_server_opened, (seed, self._topology_id)))
if _SDAM_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
@ -265,6 +268,7 @@ class Topology:
server_selection_timeout: Optional[float] = None,
address: Optional[_Address] = None,
operation_id: Optional[int] = None,
deprioritized_servers: Optional[list[Server]] = None,
) -> list[Server]:
"""Return a list of Servers matching selector, or time out.
@ -292,7 +296,12 @@ class Topology:
async with self._lock:
server_descriptions = await self._select_servers_loop(
selector, server_timeout, operation, operation_id, address
selector,
server_timeout,
operation,
operation_id,
address,
deprioritized_servers=deprioritized_servers,
)
return [
@ -306,6 +315,7 @@ class Topology:
operation: str,
operation_id: Optional[int],
address: Optional[_Address],
deprioritized_servers: Optional[list[Server]] = None,
) -> list[ServerDescription]:
"""select_servers() guts. Hold the lock when calling this."""
now = time.monotonic()
@ -324,7 +334,12 @@ class Topology:
)
server_descriptions = self._description.apply_selector(
selector, address, custom_selector=self._settings.server_selector
selector,
address,
custom_selector=self._settings.server_selector,
deprioritized_servers=[server.description for server in deprioritized_servers]
if deprioritized_servers
else None,
)
while not server_descriptions:
@ -385,9 +400,13 @@ class Topology:
operation_id: Optional[int] = None,
) -> Server:
servers = await self.select_servers(
selector, operation, server_selection_timeout, address, operation_id
selector,
operation,
server_selection_timeout,
address,
operation_id,
deprioritized_servers,
)
servers = _filter_servers(servers, deprioritized_servers)
if len(servers) == 1:
return servers[0]
server1, server2 = random.sample(servers, 2)
@ -491,6 +510,7 @@ class Topology:
suppress_event = sd_old == server_description
if self._publish_server and not suppress_event:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_server_description_changed,
@ -503,6 +523,7 @@ class Topology:
if self._publish_tp and not suppress_event:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_topology_description_changed,
@ -570,6 +591,7 @@ class Topology:
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_topology_description_changed,
@ -723,6 +745,7 @@ class Topology:
# Publish only after releasing the lock.
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._description = TopologyDescription(
TOPOLOGY_TYPE.Unknown,
{},
@ -1114,16 +1137,3 @@ def _is_stale_server_description(current_sd: ServerDescription, new_sd: ServerDe
if current_tv["processId"] != new_tv["processId"]:
return False
return current_tv["counter"] > new_tv["counter"]
def _filter_servers(
candidates: list[Server], deprioritized_servers: Optional[list[Server]] = None
) -> list[Server]:
"""Filter out deprioritized servers from a list of server candidates."""
if not deprioritized_servers:
return candidates
filtered = [server for server in candidates if server not in deprioritized_servers]
# If not possible to pick a prioritized server, return the original list
return filtered or candidates

View File

@ -160,7 +160,6 @@ def _build_credentials_tuple(
"127.0.0.1",
"::1",
"*.mongo.com",
"*.mongodbgov.net",
]
allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed)
if properties.get("ALLOWED_HOSTS", None) is not None and human_callback is None:

View File

@ -1298,8 +1298,6 @@ def _batched_write_command_impl(
# Start of payload
buf.seek(-1, 2)
# Work around some Jython weirdness.
buf.truncate()
try:
buf.write(_OP_MAP[operation])
except KeyError:

View File

@ -45,7 +45,6 @@ from cryptography.x509 import ExtendedKeyUsage as _ExtendedKeyUsage
from cryptography.x509 import ExtensionNotFound as _ExtensionNotFound
from cryptography.x509 import TLSFeature as _TLSFeature
from cryptography.x509 import TLSFeatureType as _TLSFeatureType
from cryptography.x509 import load_pem_x509_certificate as _load_pem_x509_certificate
from cryptography.x509.ocsp import OCSPCertStatus as _OCSPCertStatus
from cryptography.x509.ocsp import OCSPRequestBuilder as _OCSPRequestBuilder
from cryptography.x509.ocsp import OCSPResponseStatus as _OCSPResponseStatus
@ -102,19 +101,6 @@ _CERT_REGEX = _re.compile(
)
def _load_trusted_ca_certs(cafile: str) -> list[Certificate]:
"""Parse the tlsCAFile into a list of certificates."""
with open(cafile, "rb") as f:
data = f.read()
# Load all the certs in the file.
trusted_ca_certs = []
backend = _default_backend()
for cert_data in _re.findall(_CERT_REGEX, data):
trusted_ca_certs.append(_load_pem_x509_certificate(cert_data, backend))
return trusted_ca_certs
def _get_issuer_cert(
cert: Certificate, chain: Iterable[Certificate], trusted_ca_certs: Optional[list[Certificate]]
) -> Optional[Certificate]:

View File

@ -79,17 +79,6 @@ elif sys.platform == "win32":
# Windows patch level (e.g. 10.0.17763-SP0).
"version": ".".join(map(str, _ver[:3])) + f"-SP{_ver[-1] or '0'}",
}
elif sys.platform.startswith("java"):
_name, _ver, _arch = platform.java_ver()[-1]
_METADATA["os"] = {
# Linux, Windows 7, Mac OS X, etc.
"type": _name,
"name": _name,
# x86, x86_64, AMD64, etc.
"architecture": _arch,
# Linux kernel version, OSX version, etc.
"version": _ver,
}
else:
# Get potential alias (e.g. SunOS 5.11 becomes Solaris 2.11)
_aliased = platform.system_alias(platform.system(), platform.release(), platform.version())
@ -108,14 +97,6 @@ if platform.python_implementation().startswith("PyPy"):
"(Python %s)" % ".".join(map(str, sys.version_info)),
)
)
elif sys.platform.startswith("java"):
_METADATA["platform"] = " ".join(
(
platform.python_implementation(),
".".join(map(str, sys.version_info)),
"(%s)" % " ".join((platform.system(), platform.release())),
)
)
else:
_METADATA["platform"] = " ".join(
(platform.python_implementation(), ".".join(map(str, sys.version_info)))

View File

@ -237,8 +237,7 @@ async def _async_create_connection(address: _Address, options: PoolOptions) -> s
else:
# This likely means we tried to connect to an IPv6 only
# host with an OS/kernel or Python interpreter that doesn't
# support IPv6. The test case is Jython2.5.1 which doesn't
# support IPv6 at all.
# support IPv6.
raise OSError("getaddrinfo failed")
@ -418,8 +417,7 @@ def _create_connection(address: _Address, options: PoolOptions) -> socket.socket
else:
# This likely means we tried to connect to an IPv6 only
# host with an OS/kernel or Python interpreter that doesn't
# support IPv6. The test case is Jython2.5.1 which doesn't
# support IPv6 at all.
# support IPv6.
raise OSError("getaddrinfo failed")

View File

@ -35,7 +35,7 @@ from OpenSSL import crypto as _crypto
from pymongo.errors import ConfigurationError as _ConfigurationError
from pymongo.errors import _CertificateError # type:ignore[attr-defined]
from pymongo.ocsp_cache import _OCSPCache
from pymongo.ocsp_support import _load_trusted_ca_certs, _ocsp_callback
from pymongo.ocsp_support import _ocsp_callback
from pymongo.socket_checker import SocketChecker as _SocketChecker
from pymongo.socket_checker import _errno_from_exception
from pymongo.write_concern import validate_boolean
@ -322,10 +322,6 @@ class SSLContext:
ssl.CERT_NONE.
"""
self._ctx.load_verify_locations(cafile, capath)
# Manually load the CA certs when get_verified_chain is not available (pyopenssl<20).
if not hasattr(_SSL.Connection, "get_verified_chain"):
assert cafile is not None
self._callback_data.trusted_ca_certs = _load_trusted_ca_certs(cafile)
def _load_certifi(self) -> None:
"""Attempt to load CA certs from certifi."""

View File

@ -34,16 +34,16 @@ class Selection:
@classmethod
def from_topology_description(cls, topology_description: TopologyDescription) -> Selection:
known_servers = topology_description.known_servers
candidate_servers = topology_description.candidate_servers
primary = None
for sd in known_servers:
for sd in candidate_servers:
if sd.server_type == SERVER_TYPE.RSPrimary:
primary = sd
break
return Selection(
topology_description,
topology_description.known_servers,
topology_description.candidate_servers,
topology_description.common_wire_version,
primary,
)

View File

@ -17,12 +17,9 @@ from __future__ import annotations
import errno
import select
import sys
from typing import Any, Optional, cast
# PYTHON-2320: Jython does not fully support poll on SSL sockets,
# https://bugs.jython.org/issue2900
_HAVE_POLL = hasattr(select, "poll") and not sys.platform.startswith("java")
_HAVE_POLL = hasattr(select, "poll")
_SelectError = getattr(select, "error", OSError)

View File

@ -3307,7 +3307,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and deletes it, returning the document.
>>> db.test.count_documents({'x': 1})
@ -3317,6 +3317,10 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
>>> db.test.count_documents({'x': 1})
1
Returns ``None`` if no document matches the filter.
>>> db.test.find_one_and_delete({'_exists': False})
If multiple documents match *filter*, a *sort* can be applied.
>>> for doc in db.test.find({'x': 1}):
@ -3399,10 +3403,22 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and replaces it, returning either the
original or the replaced document.
>>> db.test.find_one({'x': 1})
{'_id': 0, 'x': 1}
>>> db.test.find_one_and_replace({'x': 1}, {'y': 2})
{'_id': 0, 'x': 1}
>>> db.test.find_one({'x': 1})
>>> db.test.find_one({'y': 2})
{'_id': 0, 'y': 2}
Returns ``None`` if no document matches the filter.
>>> db.test.find_one_and_replace({'_exists': False}, {'x': 1})
The :meth:`find_one_and_replace` method differs from
:meth:`find_one_and_update` by replacing the document matched by
*filter*, rather than modifying the existing document.
@ -3507,13 +3523,17 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and updates it, returning either the
original or the updated document.
>>> db.test.find_one({'_id': 665})
{'_id': 665, 'done': False, 'count': 25}
>>> db.test.find_one_and_update(
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
{'_id': 665, 'done': False, 'count': 25}}
{'_id': 665, 'done': False, 'count': 25}
>>> db.test.find_one({'_id': 665})
{'_id': 665, 'done': True, 'count': 26}
Returns ``None`` if no document matches the filter.

View File

@ -55,6 +55,7 @@ from pymongo.message import (
_RawBatchQuery,
)
from pymongo.response import PinnedResponse
from pymongo.synchronous.helpers import next
from pymongo.typings import _Address, _CollationIn, _DocumentOut, _DocumentType
from pymongo.write_concern import validate_boolean

View File

@ -701,7 +701,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
.. versionadded:: 3.9
.. _aggregation pipeline:
https://mongodb.com/docs/manual/reference/operator/aggregation-pipeline
https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
.. _aggregate command:
https://mongodb.com/docs/manual/reference/command/aggregate

View File

@ -710,7 +710,10 @@ class ClientEncryption(Generic[_DocumentType]):
self._encryption = ExplicitEncrypter(
self._io_callbacks,
_create_mongocrypt_options(
kms_providers=kms_providers, schema_map=None, key_expiration_ms=key_expiration_ms
kms_providers=kms_providers,
schema_map=None,
key_expiration_ms=key_expiration_ms,
bypass_encryption=True, # Don't load crypt_shared
),
)
# Use the same key vault collection as the callback.

View File

@ -16,9 +16,11 @@
from __future__ import annotations
import asyncio
import builtins
import functools
import random
import socket
import sys
import time as time # noqa: PLC0414 # needed in sync version
from typing import (
Any,
@ -208,3 +210,17 @@ def _getaddrinfo(
return loop.getaddrinfo(host, port, **kwargs) # type: ignore[return-value]
else:
return socket.getaddrinfo(host, port, **kwargs)
if sys.version_info >= (3, 10):
next = builtins.next
iter = builtins.iter
else:
def next(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
return cls.__next__()
def iter(cls: Any) -> Any:
"""Compatibility function until we drop 3.9 support: https://docs.python.org/3/library/functions.html#next."""
return cls.__iter__()

View File

@ -2837,7 +2837,7 @@ class _ClientConnectionRetryable(Generic[T]):
if self._last_error is None:
self._last_error = exc
if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded:
if self._server is not None:
self._deprioritized_servers.append(self._server)
self._always_retryable = always_retryable

View File

@ -108,21 +108,6 @@ if TYPE_CHECKING:
from pymongo.typings import _Address, _CollationIn
from pymongo.write_concern import WriteConcern
try:
from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl
def _set_non_inheritable_non_atomic(fd: int) -> None:
"""Set the close-on-exec flag on the given file descriptor."""
flags = fcntl(fd, F_GETFD)
fcntl(fd, F_SETFD, flags | FD_CLOEXEC)
except ImportError:
# Windows, various platforms we don't claim to support
# (Jython, IronPython, ..), systems that don't provide
# everything we need from fcntl, etc.
def _set_non_inheritable_non_atomic(fd: int) -> None: # noqa: ARG001
"""Dummy function for platforms that don't provide fcntl."""
_IS_SYNC = True
@ -708,8 +693,6 @@ class PoolState:
CLOSED = 3
# Do *not* explicitly inherit from object or Jython won't call __del__
# https://bugs.jython.org/issue1057
class Pool:
def __init__(
self,

View File

@ -111,7 +111,7 @@ class Topology:
self._publish_tp = self._listeners is not None and self._listeners.enabled_for_topology
# Create events queue if there are publishers.
self._events = None
self._events: queue.Queue[Any] | None = None
self.__events_executor: Any = None
if self._publish_server or self._publish_tp:
@ -126,6 +126,7 @@ class Topology:
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._events.put((self._listeners.publish_topology_opened, (self._topology_id,)))
self._settings = topology_settings
topology_description = TopologyDescription(
@ -143,6 +144,7 @@ class Topology:
)
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_topology_description_changed,
@ -161,6 +163,7 @@ class Topology:
for seed in topology_settings.seeds:
if self._publish_server:
assert self._events is not None
assert self._listeners is not None
self._events.put((self._listeners.publish_server_opened, (seed, self._topology_id)))
if _SDAM_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
@ -265,6 +268,7 @@ class Topology:
server_selection_timeout: Optional[float] = None,
address: Optional[_Address] = None,
operation_id: Optional[int] = None,
deprioritized_servers: Optional[list[Server]] = None,
) -> list[Server]:
"""Return a list of Servers matching selector, or time out.
@ -292,7 +296,12 @@ class Topology:
with self._lock:
server_descriptions = self._select_servers_loop(
selector, server_timeout, operation, operation_id, address
selector,
server_timeout,
operation,
operation_id,
address,
deprioritized_servers=deprioritized_servers,
)
return [
@ -306,6 +315,7 @@ class Topology:
operation: str,
operation_id: Optional[int],
address: Optional[_Address],
deprioritized_servers: Optional[list[Server]] = None,
) -> list[ServerDescription]:
"""select_servers() guts. Hold the lock when calling this."""
now = time.monotonic()
@ -324,7 +334,12 @@ class Topology:
)
server_descriptions = self._description.apply_selector(
selector, address, custom_selector=self._settings.server_selector
selector,
address,
custom_selector=self._settings.server_selector,
deprioritized_servers=[server.description for server in deprioritized_servers]
if deprioritized_servers
else None,
)
while not server_descriptions:
@ -385,9 +400,13 @@ class Topology:
operation_id: Optional[int] = None,
) -> Server:
servers = self.select_servers(
selector, operation, server_selection_timeout, address, operation_id
selector,
operation,
server_selection_timeout,
address,
operation_id,
deprioritized_servers,
)
servers = _filter_servers(servers, deprioritized_servers)
if len(servers) == 1:
return servers[0]
server1, server2 = random.sample(servers, 2)
@ -491,6 +510,7 @@ class Topology:
suppress_event = sd_old == server_description
if self._publish_server and not suppress_event:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_server_description_changed,
@ -503,6 +523,7 @@ class Topology:
if self._publish_tp and not suppress_event:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_topology_description_changed,
@ -570,6 +591,7 @@ class Topology:
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._events.put(
(
self._listeners.publish_topology_description_changed,
@ -721,6 +743,7 @@ class Topology:
# Publish only after releasing the lock.
if self._publish_tp:
assert self._events is not None
assert self._listeners is not None
self._description = TopologyDescription(
TOPOLOGY_TYPE.Unknown,
{},
@ -1112,16 +1135,3 @@ def _is_stale_server_description(current_sd: ServerDescription, new_sd: ServerDe
if current_tv["processId"] != new_tv["processId"]:
return False
return current_tv["counter"] > new_tv["counter"]
def _filter_servers(
candidates: list[Server], deprioritized_servers: Optional[list[Server]] = None
) -> list[Server]:
"""Filter out deprioritized servers from a list of server candidates."""
if not deprioritized_servers:
return candidates
filtered = [server for server in candidates if server not in deprioritized_servers]
# If not possible to pick a prioritized server, return the original list
return filtered or candidates

View File

@ -85,6 +85,7 @@ class TopologyDescription:
self._server_descriptions = server_descriptions
self._max_set_version = max_set_version
self._max_election_id = max_election_id
self._candidate_servers = list(self._server_descriptions.values())
# The heartbeat_frequency is used in staleness estimates.
self._topology_settings = topology_settings
@ -248,6 +249,11 @@ class TopologyDescription:
"""List of readable Servers."""
return [s for s in self._server_descriptions.values() if s.is_readable]
@property
def candidate_servers(self) -> list[ServerDescription]:
"""List of Servers excluding deprioritized servers."""
return self._candidate_servers
@property
def common_wire_version(self) -> Optional[int]:
"""Minimum of all servers' max wire versions, or None."""
@ -283,11 +289,27 @@ class TopologyDescription:
if (cast(float, s.round_trip_time) - fastest) <= threshold
]
def _filter_servers(
self, deprioritized_servers: Optional[list[ServerDescription]] = None
) -> None:
"""Filter out deprioritized servers from a list of server candidates."""
if not deprioritized_servers:
self._candidate_servers = self.known_servers
else:
deprioritized_addresses = {sd.address for sd in deprioritized_servers}
filtered = [
server
for server in self.known_servers
if server.address not in deprioritized_addresses
]
self._candidate_servers = filtered or self.known_servers
def apply_selector(
self,
selector: Any,
address: Optional[_Address] = None,
custom_selector: Optional[_ServerSelector] = None,
deprioritized_servers: Optional[list[ServerDescription]] = None,
) -> list[ServerDescription]:
"""List of servers matching the provided selector(s).
@ -322,16 +344,25 @@ class TopologyDescription:
if address:
# Ignore selectors when explicit address is requested.
description = self.server_descriptions().get(address)
return [description] if description else []
return [description] if description and description.is_server_type_known else []
self._filter_servers(deprioritized_servers)
# Primary selection fast path.
if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and type(selector) is Primary:
for sd in self._server_descriptions.values():
for sd in self._candidate_servers:
if sd.server_type == SERVER_TYPE.RSPrimary:
sds = [sd]
if custom_selector:
sds = custom_selector(sds)
return sds
# All primaries are deprioritized
if deprioritized_servers:
for sd in deprioritized_servers:
if sd.server_type == SERVER_TYPE.RSPrimary:
sds = [sd]
if custom_selector:
sds = custom_selector(sds)
return sds
# No primary found, return an empty list.
return []
@ -339,6 +370,11 @@ class TopologyDescription:
# Ignore read preference for sharded clusters.
if self.topology_type != TOPOLOGY_TYPE.Sharded:
selection = selector(selection)
# No suitable servers found, apply preference again but include deprioritized servers.
if not selection and deprioritized_servers:
self._filter_servers(None)
selection = Selection.from_topology_description(self)
selection = selector(selection)
# Apply custom selector followed by localThresholdMS.
if custom_selector is not None and selection:

View File

@ -48,21 +48,21 @@ Tracker = "https://jira.mongodb.org/projects/PYTHON/issues"
[dependency-groups]
dev = []
pip = ["pip"]
gevent = ["gevent>=20.6.0"]
pip = ["pip>=20.2"]
gevent = ["gevent>=21.12"]
coverage = [
"pytest-cov",
"coverage>=5,<=7.10.6"
"pytest-cov>=4.0.0",
"coverage[toml]>=5,<=7.10.7"
]
mockupdb = [
"mockupdb@git+https://github.com/mongodb-labs/mongo-mockup-db@master"
]
perf = ["simplejson>=3.17.0"]
typing = [
"mypy==1.18.2",
"mypy==1.19.1",
"pyright==1.1.407",
"typing_extensions",
"pip"
"typing_extensions>=3.7.4.2",
"pip>=20.2"
]
# Used to call hatch_build.py

View File

@ -3,4 +3,4 @@ sphinx_rtd_theme>=2,<4
readthedocs-sphinx-search~=0.3
sphinxcontrib-shellcheck>=1,<2
sphinx-autobuild>=2020.9.1
furo==2025.9.25
furo==2025.12.19

View File

@ -1,2 +1,2 @@
pykerberos;os.name!='nt'
pykerberos>=1.2.4;os.name!='nt'
winkerberos>=0.5.0;os.name=='nt'

View File

@ -4,9 +4,10 @@
# service_identity 18.1.0 introduced support for IP addr matching.
# Fallback to certifi on Windows if we can't load CA certs from the system
# store and just use certifi on macOS.
# pyopenssl, cryptography, and service_identity must be set in tandem.
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
certifi>=2023.7.22;os.name=='nt' or sys_platform=='darwin'
pyopenssl>=17.2.0
requests<3.0.0
cryptography>=2.5
service_identity>=18.1.0
pyopenssl>=23.2.0
requests>=2.23.0,<3.0
cryptography>=42.0.0
service_identity>=23.1.0

View File

@ -1 +1 @@
python-snappy
python-snappy>=0.6.0

307
sbom.json
View File

@ -1,159 +1,202 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": "urn:uuid:f91a87bf-a37f-4c1e-805f-142f60b2c960",
"version": 1,
"metadata": {
"timestamp": "2025-11-20T21:30:34Z",
"tools": {
"components": [
{
"group": "@cyclonedx",
"name": "cdxgen",
"version": "11.11.0",
"purl": "pkg:npm/%40cyclonedx/cdxgen@11.11.0",
"type": "application",
"bom-ref": "pkg:npm/@cyclonedx/cdxgen@11.11.0",
"author": "OWASP Foundation",
"publisher": "OWASP Foundation"
}
]
},
"authors": [
{
"name": "OWASP Foundation"
}
],
"lifecycles": [
{
"phase": "build"
}
],
"component": {
"name": "pymongo",
"description": "PyMongo - the Official MongoDB Python driver",
"authors": [
{
"name": "The MongoDB Python Team"
}
],
"tags": [
"bson",
"gridfs",
"mongo",
"mongodb",
"pymongo"
],
"properties": [
{
"name": "cdx:pypi:requiresPython",
"value": ">=3.9"
},
{
"name": "SrcFile",
"value": "/home/runner/work/mongo-python-driver/mongo-python-driver/pyproject.toml"
}
],
"type": "application",
"bom-ref": "pkg:pypi/pymongo@latest",
"purl": "pkg:pypi/pymongo@latest",
"version": "latest",
"licenses": [
{
"license": {
"id": "Apache-2.0",
"url": "https://opensource.org/licenses/Apache-2.0"
}
}
]
},
"properties": [
{
"name": "cdx:bom:componentTypes",
"value": "pypi"
},
{
"name": "cdx:bom:componentSrcFiles",
"value": "pyproject.toml"
}
]
},
"components": [
{
"group": "",
"name": "pymongo",
"version": "latest",
"purl": "pkg:pypi/pymongo@latest",
"type": "library",
"bom-ref": "pkg:pypi/pymongo@latest",
"properties": [
"bom-ref": "dnspython==2.8.0",
"description": "DNS toolkit",
"externalReferences": [
{
"name": "SrcFile",
"value": "pyproject.toml"
"comment": "from packaging metadata Project-URL: documentation",
"type": "documentation",
"url": "https://dnspython.readthedocs.io/en/stable/"
},
{
"comment": "from packaging metadata Project-URL: issues",
"type": "issue-tracker",
"url": "https://github.com/rthalley/dnspython/issues"
},
{
"comment": "from packaging metadata Project-URL: repository",
"type": "vcs",
"url": "https://github.com/rthalley/dnspython.git"
},
{
"comment": "from packaging metadata Project-URL: homepage",
"type": "website",
"url": "https://www.dnspython.org"
}
],
"evidence": {
"identity": {
"field": "purl",
"confidence": 1,
"methods": [
{
"technique": "instrumentation",
"confidence": 1,
"value": "/home/runner/work/mongo-python-driver/mongo-python-driver/.venv"
}
]
}
}
},
{
"author": "Bob Halley <halley@dnspython.org>",
"group": "",
"name": "dnspython",
"version": "2.8.0",
"description": "DNS toolkit",
"licenses": [
{
"license": {
"id": "ISC",
"url": "https://opensource.org/licenses/ISC"
"id": "ISC"
}
}
],
"name": "dnspython",
"purl": "pkg:pypi/dnspython@2.8.0",
"type": "library",
"bom-ref": "pkg:pypi/dnspython@2.8.0",
"properties": [
"version": "2.8.0"
},
{
"bom-ref": "pymongo==4.16.0.dev0",
"description": "PyMongo - the Official MongoDB Python driver",
"externalReferences": [
{
"name": "SrcFile",
"value": "pyproject.toml"
"comment": "PackageSource: Local",
"type": "distribution",
"url": "file:///home/runner/work/mongo-python-driver/mongo-python-driver"
},
{
"comment": "from packaging metadata Project-URL: Documentation",
"type": "documentation",
"url": "https://www.mongodb.com/docs/languages/python/pymongo-driver/current/"
},
{
"comment": "from packaging metadata Project-URL: Tracker",
"type": "issue-tracker",
"url": "https://jira.mongodb.org/projects/PYTHON/issues"
},
{
"comment": "from packaging metadata Project-URL: Source",
"type": "other",
"url": "https://github.com/mongodb/mongo-python-driver"
},
{
"comment": "from packaging metadata Project-URL: Homepage",
"type": "website",
"url": "https://www.mongodb.org"
}
],
"evidence": {
"identity": {
"field": "purl",
"confidence": 1,
"methods": [
{
"technique": "instrumentation",
"confidence": 1,
"value": "/home/runner/work/mongo-python-driver/mongo-python-driver/.venv"
}
]
"licenses": [
{
"license": {
"id": "Apache-2.0"
}
}
}
],
"name": "pymongo",
"type": "library",
"version": "4.16.0.dev0",
"purl": "pkg:pypi/pymongo@4.16.0.dev0"
}
],
"dependencies": [
{
"ref": "pkg:pypi/dnspython@2.8.0",
"dependsOn": []
"ref": "dnspython==2.8.0"
},
{
"ref": "pkg:pypi/pymongo@latest",
"dependsOn": [
"pkg:pypi/dnspython@2.8.0"
"dnspython==2.8.0"
],
"ref": "pymongo==4.16.0.dev0"
}
],
"metadata": {
"timestamp": "2025-11-24T16:21:47.249880+00:00",
"tools": {
"components": [
{
"description": "CycloneDX Software Bill of Materials (SBOM) generator for Python projects and environments",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/CycloneDX/cyclonedx-python/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/cyclonedx-bom/"
},
{
"type": "documentation",
"url": "https://cyclonedx-bom-tool.readthedocs.io/"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-python/issues"
},
{
"type": "license",
"url": "https://github.com/CycloneDX/cyclonedx-python/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/CycloneDX/cyclonedx-python/blob/main/CHANGELOG.md"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-python/"
},
{
"type": "website",
"url": "https://github.com/CycloneDX/cyclonedx-python/#readme"
}
],
"group": "CycloneDX",
"licenses": [
{
"license": {
"id": "Apache-2.0"
}
}
],
"name": "cyclonedx-py",
"type": "application",
"version": "7.2.1"
},
{
"description": "Python library for CycloneDX",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/cyclonedx-python-lib/"
},
{
"type": "documentation",
"url": "https://cyclonedx-python-library.readthedocs.io/"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
},
{
"type": "license",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
},
{
"type": "website",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme"
}
],
"group": "CycloneDX",
"licenses": [
{
"license": {
"id": "Apache-2.0"
}
}
],
"name": "cyclonedx-python-lib",
"type": "library",
"version": "11.5.0"
}
]
}
]
}
},
"serialNumber": "urn:uuid:7a19d697-d41e-4e88-b953-4bccb5d79937",
"version": 1,
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.5"
}

View File

@ -482,7 +482,7 @@ class ClientContext:
def drop_user(self, dbname, user):
self.client[dbname].command("dropUser", user, writeConcern={"w": self.w})
def require_connection(self, func):
def require_connection(self, func: Any) -> Any:
"""Run a test only if we can connect to MongoDB."""
return self._require(
lambda: True, # _require checks if we're connected
@ -552,7 +552,7 @@ class ClientContext:
lambda: not self.fips_enabled, "Test cannot run on a FIPS-enabled host", func=func
)
def require_replica_set(self, func):
def require_replica_set(self, func: Any) -> Any:
"""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)
@ -638,7 +638,7 @@ class ClientContext:
lambda: self.load_balancer, "Must be connected to a load balancer", func=func
)
def require_no_load_balancer(self, func):
def require_no_load_balancer(self, func: Any) -> Any:
"""Run a test only if the client is not connected to a load balancer."""
return self._require(
lambda: not self.load_balancer, "Must not be connected to a load balancer", func=func
@ -687,7 +687,7 @@ class ClientContext:
lambda: self.test_commands_enabled, "Test commands must be enabled", func=func
)
def require_failCommand_fail_point(self, func):
def require_failCommand_fail_point(self, func: Any) -> Any:
"""Run a test only if the server supports the failCommand fail
point.
"""

View File

@ -482,7 +482,7 @@ class AsyncClientContext:
async def drop_user(self, dbname, user):
await self.client[dbname].command("dropUser", user, writeConcern={"w": self.w})
def require_connection(self, func):
def require_connection(self, func: Any) -> Any:
"""Run a test only if we can connect to MongoDB."""
return self._require(
lambda: True, # _require checks if we're connected
@ -552,7 +552,7 @@ class AsyncClientContext:
lambda: not self.fips_enabled, "Test cannot run on a FIPS-enabled host", func=func
)
def require_replica_set(self, func):
def require_replica_set(self, func: Any) -> Any:
"""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)
@ -638,7 +638,7 @@ class AsyncClientContext:
lambda: self.load_balancer, "Must be connected to a load balancer", func=func
)
def require_no_load_balancer(self, func):
def require_no_load_balancer(self, func: Any) -> Any:
"""Run a test only if the client is not connected to a load balancer."""
return self._require(
lambda: not self.load_balancer, "Must not be connected to a load balancer", func=func
@ -687,7 +687,7 @@ class AsyncClientContext:
lambda: self.test_commands_enabled, "Test commands must be enabled", func=func
)
def require_failCommand_fail_point(self, func):
def require_failCommand_fail_point(self, func: Any) -> Any:
"""Run a test only if the server supports the failCommand fail
point.
"""

View File

@ -30,7 +30,7 @@ import pytest
sys.path[0:0] = [""]
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.utils_shared import EventListener, OvertCommandListener
from bson import SON
@ -54,14 +54,13 @@ from pymongo.synchronous.uri_parser import parse_uri
_IS_SYNC = False
ROOT = Path(__file__).parent.parent.resolve()
TEST_PATH = ROOT / "auth" / "unified"
ENVIRON = os.environ.get("OIDC_ENV", "test")
DOMAIN = os.environ.get("OIDC_DOMAIN", "")
TOKEN_DIR = os.environ.get("OIDC_TOKEN_DIR", "")
TOKEN_FILE = os.environ.get("OIDC_TOKEN_FILE", "")
# Generate unified tests.
globals().update(generate_test_classes(str(TEST_PATH), module=__name__))
globals().update(generate_test_classes(get_test_path("auth", "unified"), module=__name__))
pytestmark = pytest.mark.auth_oidc

View File

@ -27,7 +27,7 @@ import pytest
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from pymongo import AsyncMongoClient
from pymongo.auth_oidc_shared import OIDCCallback
@ -35,8 +35,7 @@ from pymongo.auth_oidc_shared import OIDCCallback
pytestmark = pytest.mark.auth
_IS_SYNC = False
_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "auth")
_TEST_PATH = get_test_path("auth")
class TestAuthSpec(AsyncPyMongoTestCase):

View File

@ -35,7 +35,7 @@ from test.asynchronous import (
async_client_context,
unittest,
)
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.utils_shared import (
AllowListEventListener,
EventListener,
@ -48,6 +48,7 @@ from bson.binary import ALL_UUID_REPRESENTATIONS, PYTHON_LEGACY, STANDARD, Binar
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument
from pymongo import AsyncMongoClient
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.helpers import anext
from pymongo.errors import (
InvalidOperation,
OperationFailure,
@ -771,8 +772,8 @@ class ProseSpecTestsMixin:
class TestClusterAsyncChangeStream(TestAsyncChangeStreamBase, APITestsMixin):
dbs: list
@async_client_context.require_version_min(4, 2, 0)
@async_client_context.require_change_streams
@async_client_context.require_version_min(4, 2, 0) # type:ignore[untyped-decorator]
@async_client_context.require_change_streams # type:ignore[untyped-decorator]
async def asyncSetUp(self) -> None:
await super().asyncSetUp()
self.dbs = [self.db, self.client.pymongo_test_2]
@ -831,8 +832,8 @@ class TestClusterAsyncChangeStream(TestAsyncChangeStreamBase, APITestsMixin):
class TestAsyncDatabaseAsyncChangeStream(TestAsyncChangeStreamBase, APITestsMixin):
@async_client_context.require_version_min(4, 2, 0)
@async_client_context.require_change_streams
@async_client_context.require_version_min(4, 2, 0) # type:ignore[untyped-decorator]
@async_client_context.require_change_streams # type:ignore[untyped-decorator]
async def asyncSetUp(self) -> None:
await super().asyncSetUp()
@ -1143,12 +1144,9 @@ class TestAllLegacyScenarios(AsyncIntegrationTest):
self.listener.reset()
_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "change_streams")
globals().update(
generate_test_classes(
os.path.join(_TEST_PATH, "unified"),
get_test_path("change_streams", "unified"),
module=__name__,
)
)

View File

@ -92,6 +92,7 @@ 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,
@ -1066,9 +1067,6 @@ class TestClient(AsyncIntegrationTest):
await coll.count_documents({})
async def test_close_kills_cursors(self):
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()
# Kill any cursors possibly queued up by previous tests.
gc.collect()
@ -1088,7 +1086,7 @@ class TestClient(AsyncIntegrationTest):
cursor = await coll.aggregate([], batchSize=10)
self.assertTrue(bool(await anext(cursor)))
del cursor
# Required for PyPy, Jython and other Python implementations that
# Required for PyPy and other Python implementations that
# don't use reference counting garbage collection.
gc.collect()
@ -1456,12 +1454,6 @@ class TestClient(AsyncIntegrationTest):
@async_client_context.require_sync
def test_interrupt_signal(self):
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")
if is_greenthread_patched():
raise SkipTest("Can't reliably test interrupts with green threads")
@ -2406,7 +2398,7 @@ class TestExhaustCursor(AsyncIntegrationTest):
client = self.async_rs_or_single_client()
self.addCleanup(client.close)
coll = client.pymongo_test.test
pool = async_get_pool(client)
pool = async_get_pool(client) # type:ignore
# Patch the pool to delay the connect method.
def delayed_connect(*args, **kwargs):

View File

@ -19,7 +19,7 @@ import pathlib
import time
import unittest
from test.asynchronous import AsyncIntegrationTest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.utils_shared import CMAPListener
from typing import Any, Optional
@ -40,16 +40,8 @@ pytestmark = pytest.mark.mockupdb
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "handshake", "unified")
else:
_TEST_PATH = os.path.join(
pathlib.Path(__file__).resolve().parent.parent, "handshake", "unified"
)
# Generate unified tests.
globals().update(generate_test_classes(_TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("handshake", "unified"), module=__name__))
def _get_handshake_driver_info(request):

View File

@ -21,6 +21,7 @@ from test.asynchronous import AsyncIntegrationTest, async_client_context, unitte
from test.utils_shared import EventListener, OvertCommandListener
from typing import Any
from pymongo.asynchronous.helpers import anext
from pymongo.collation import (
Collation,
CollationAlternate,

View File

@ -25,6 +25,7 @@ from test.asynchronous.utils import async_get_pool, async_is_mongos
from typing import Any, Iterable, no_type_check
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
sys.path[0:0] = [""]

View File

@ -22,20 +22,12 @@ import sys
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "collection_management")
else:
_TEST_PATH = os.path.join(
pathlib.Path(__file__).resolve().parent.parent, "collection_management"
)
# Generate unified tests.
globals().update(generate_test_classes(_TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("collection_management"), module=__name__))
if __name__ == "__main__":
unittest.main()

View File

@ -22,20 +22,13 @@ import sys
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "command_logging")
else:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "command_logging")
globals().update(
generate_test_classes(
_TEST_PATH,
get_test_path("command_logging"),
module=__name__,
)
)

View File

@ -22,20 +22,13 @@ import sys
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "command_monitoring")
else:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "command_monitoring")
globals().update(
generate_test_classes(
_TEST_PATH,
get_test_path("command_monitoring"),
module=__name__,
)
)

View File

@ -22,20 +22,13 @@ import sys
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "connection_logging")
else:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "connection_logging")
globals().update(
generate_test_classes(
_TEST_PATH,
get_test_path("connection_logging"),
module=__name__,
)
)

View File

@ -22,18 +22,12 @@ import sys
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "crud", "unified")
else:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "crud", "unified")
# Generate unified tests.
globals().update(generate_test_classes(_TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("crud", "unified"), module=__name__))
if __name__ == "__main__":
unittest.main()

View File

@ -22,7 +22,7 @@ from pathlib import Path
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.asynchronous.utils import flaky
import pymongo
@ -31,14 +31,8 @@ from pymongo.errors import PyMongoError
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "csot")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "csot")
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("csot"), module=__name__))
class TestCSOT(AsyncIntegrationTest):

View File

@ -46,6 +46,7 @@ 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
from pymongo.collation import Collation
from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure, PyMongoError
from pymongo.operations import _IndexList

View File

@ -53,6 +53,7 @@ from bson.errors import InvalidDocument
from bson.int64 import Int64
from bson.raw_bson import RawBSONDocument
from pymongo.asynchronous.collection import ReturnDocument
from pymongo.asynchronous.helpers import anext
from pymongo.errors import DuplicateKeyError
from pymongo.message import _CursorAddress

View File

@ -42,6 +42,7 @@ from pymongo import helpers_shared
from pymongo.asynchronous import auth
from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.errors import (
CollectionInvalid,
@ -475,7 +476,7 @@ class TestDatabase(AsyncIntegrationTest):
# when you iterate key/value pairs in a document.
# This isn't reliable since python dicts don't
# guarantee any particular order. This will never
# work right in Jython or any Python or environment
# work right in any Python or environment
# with hash randomization enabled (e.g. tox).
db = self.client.pymongo_test
await db.test.drop()

View File

@ -42,7 +42,7 @@ from test.asynchronous import (
unittest,
)
from test.asynchronous.pymongo_mocks import DummyMonitor
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.asynchronous.utils import (
async_get_pool,
)
@ -83,14 +83,7 @@ from pymongo.topology_description import TOPOLOGY_TYPE
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
SDAM_PATH = os.path.join(Path(__file__).resolve().parent, "discovery_and_monitoring")
else:
SDAM_PATH = os.path.join(
Path(__file__).resolve().parent.parent,
"discovery_and_monitoring",
)
SDAM_PATH = get_test_path("discovery_and_monitoring")
async def create_mock_topology(uri, monitor_class=DummyMonitor):

View File

@ -33,7 +33,6 @@ import warnings
from test.asynchronous import AsyncIntegrationTest, AsyncPyMongoTestCase, async_client_context
from test.asynchronous.test_bulk import AsyncBulkTestBase
from test.asynchronous.utils import flaky
from test.asynchronous.utils_spec_runner import AsyncSpecRunner, AsyncSpecTestCreator
from threading import Thread
from typing import Any, Dict, Mapping, Optional
@ -54,8 +53,7 @@ from test import (
unittest,
)
from test.asynchronous.test_bulk import AsyncBulkTestBase
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.utils_spec_runner import AsyncSpecRunner
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.helpers_shared import (
ALL_KMS_PROVIDERS,
AWS_CREDS,
@ -86,6 +84,7 @@ from bson.son import SON
from pymongo import ReadPreference
from pymongo.asynchronous import encryption
from pymongo.asynchronous.encryption import Algorithm, AsyncClientEncryption, QueryType
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.cursor_shared import CursorType
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts, RangeOpts, TextOpts
@ -233,7 +232,7 @@ class AsyncEncryptionIntegrationTest(AsyncIntegrationTest):
"""Base class for encryption integration tests."""
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
@async_client_context.require_version_min(4, 2, -1)
@async_client_context.require_version_min(4, 2, -1) # type:ignore[untyped-decorator]
async def asyncSetUp(self) -> None:
await super().asyncSetUp()
@ -275,11 +274,7 @@ class AsyncEncryptionIntegrationTest(AsyncIntegrationTest):
# Location of JSON test files.
if _IS_SYNC:
BASE = os.path.join(pathlib.Path(__file__).resolve().parent, "client-side-encryption")
else:
BASE = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "client-side-encryption")
BASE = get_test_path("client-side-encryption")
SPEC_PATH = os.path.join(BASE, "spec")
OPTS = CodecOptions()
@ -624,125 +619,6 @@ AWS_TEMP_NO_SESSION_CREDS = {
}
class AsyncTestSpec(AsyncSpecRunner):
@classmethod
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
async def _setup_class(cls):
await super()._setup_class()
def parse_auto_encrypt_opts(self, opts):
"""Parse clientOptions.autoEncryptOpts."""
opts = camel_to_snake_args(opts)
kms_providers = opts["kms_providers"]
if "aws" in kms_providers:
kms_providers["aws"] = AWS_CREDS
if not any(AWS_CREDS.values()):
self.skipTest("AWS environment credentials are not set")
if "awsTemporary" in kms_providers:
kms_providers["aws"] = AWS_TEMP_CREDS
del kms_providers["awsTemporary"]
if not any(AWS_TEMP_CREDS.values()):
self.skipTest("AWS Temp environment credentials are not set")
if "awsTemporaryNoSessionToken" in kms_providers:
kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS
del kms_providers["awsTemporaryNoSessionToken"]
if not any(AWS_TEMP_NO_SESSION_CREDS.values()):
self.skipTest("AWS Temp environment credentials are not set")
if "azure" in kms_providers:
kms_providers["azure"] = AZURE_CREDS
if not any(AZURE_CREDS.values()):
self.skipTest("Azure environment credentials are not set")
if "gcp" in kms_providers:
kms_providers["gcp"] = GCP_CREDS
if not any(AZURE_CREDS.values()):
self.skipTest("GCP environment credentials are not set")
if "kmip" in kms_providers:
kms_providers["kmip"] = KMIP_CREDS
opts["kms_tls_options"] = DEFAULT_KMS_TLS
if "key_vault_namespace" not in opts:
opts["key_vault_namespace"] = "keyvault.datakeys"
if "extra_options" in opts:
opts.update(camel_to_snake_args(opts.pop("extra_options")))
opts = dict(opts)
return AutoEncryptionOpts(**opts)
def parse_client_options(self, opts):
"""Override clientOptions parsing to support autoEncryptOpts."""
encrypt_opts = opts.pop("autoEncryptOpts", None)
if encrypt_opts:
opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts)
return super().parse_client_options(opts)
def get_object_name(self, op):
"""Default object is collection."""
return op.get("object", "collection")
def maybe_skip_scenario(self, test):
super().maybe_skip_scenario(test)
desc = test["description"].lower()
if (
"timeoutms applied to listcollections to get collection schema" in desc
and sys.platform in ("win32", "darwin")
):
self.skipTest("PYTHON-3706 flaky test on Windows/macOS")
if "type=symbol" in desc:
self.skipTest("PyMongo does not support the symbol type")
if "timeoutms applied to listcollections to get collection schema" in desc and not _IS_SYNC:
self.skipTest("PYTHON-4844 flaky test on async")
async def setup_scenario(self, scenario_def):
"""Override a test's setup."""
key_vault_data = scenario_def["key_vault_data"]
encrypted_fields = scenario_def["encrypted_fields"]
json_schema = scenario_def["json_schema"]
data = scenario_def["data"]
coll = async_client_context.client.get_database("keyvault", codec_options=OPTS)["datakeys"]
await coll.delete_many({})
if key_vault_data:
await coll.insert_many(key_vault_data)
db_name = self.get_scenario_db_name(scenario_def)
coll_name = self.get_scenario_coll_name(scenario_def)
db = async_client_context.client.get_database(db_name, codec_options=OPTS)
await db.drop_collection(coll_name, encrypted_fields=encrypted_fields)
wc = WriteConcern(w="majority")
kwargs: Dict[str, Any] = {}
if json_schema:
kwargs["validator"] = {"$jsonSchema": json_schema}
kwargs["codec_options"] = OPTS
if not data:
kwargs["write_concern"] = wc
if encrypted_fields:
kwargs["encryptedFields"] = encrypted_fields
await db.create_collection(coll_name, **kwargs)
coll = db[coll_name]
if data:
# Load data.
await coll.with_options(write_concern=wc).insert_many(scenario_def["data"])
def allowable_errors(self, op):
"""Override expected error classes."""
errors = super().allowable_errors(op)
# An updateOne test expects encryption to error when no $ operator
# appears but pymongo raises a client side ValueError in this case.
if op["name"] == "updateOne":
errors += (ValueError,)
return errors
def create_test(scenario_def, test, name):
@async_client_context.require_test_commands
async def run_scenario(self):
await self.run_scenario(scenario_def, test)
return run_scenario
test_creator = AsyncSpecTestCreator(create_test, AsyncTestSpec, os.path.join(SPEC_PATH, "legacy"))
test_creator.create_tests()
if _HAVE_PYMONGOCRYPT:
globals().update(
generate_test_classes(

View File

@ -29,6 +29,7 @@ from test.asynchronous import AsyncIntegrationTest, async_client_context, unitte
from test.utils_shared import async_wait_until
import pymongo
from pymongo.asynchronous.helpers import anext
from pymongo.errors import ConnectionFailure, OperationFailure
from pymongo.read_concern import ReadConcern
from pymongo.read_preferences import ReadPreference

View File

@ -47,6 +47,7 @@ from gridfs.asynchronous.grid_file import (
)
from gridfs.errors import NoFile
from pymongo import AsyncMongoClient
from pymongo.asynchronous.helpers import aiter, anext
from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
from pymongo.message import _CursorAddress

View File

@ -22,18 +22,12 @@ from pathlib import Path
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "gridfs")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "gridfs")
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("gridfs"), module=__name__))
if __name__ == "__main__":
unittest.main()

View File

@ -28,7 +28,7 @@ import pytest
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest, AsyncPyMongoTestCase, unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.utils_shared import AllowListEventListener, OvertCommandListener
from pymongo.errors import OperationFailure
@ -40,12 +40,6 @@ _IS_SYNC = False
pytestmark = pytest.mark.search_index
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "index_management")
else:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "index_management")
_NAME = "test-search-index"
@ -370,7 +364,7 @@ class TestSearchIndexProse(SearchIndexIntegrationBase):
globals().update(
generate_test_classes(
_TEST_PATH,
get_test_path("index_management"),
module=__name__,
)
)

View File

@ -30,24 +30,20 @@ import pytest
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.utils_shared import (
async_wait_until,
create_async_event,
)
from pymongo.asynchronous.helpers import anext
_IS_SYNC = False
pytestmark = pytest.mark.load_balancer
# Location of JSON test specifications.
if _IS_SYNC:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "load_balancer")
else:
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "load_balancer")
# Generate unified tests.
globals().update(generate_test_classes(_TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("load_balancer"), module=__name__))
class TestLB(AsyncIntegrationTest):

View File

@ -40,6 +40,7 @@ from bson.objectid import ObjectId
from bson.son import SON
from pymongo import CursorType, DeleteOne, InsertOne, UpdateOne, monitoring
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.helpers import anext
from pymongo.errors import AutoReconnect, NotPrimaryError, OperationFailure
from pymongo.read_preferences import ReadPreference
from pymongo.write_concern import WriteConcern

View File

@ -42,6 +42,7 @@ from test.utils_shared import (
from test.version import Version
from bson.son import SON
from pymongo.asynchronous.helpers import anext
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.errors import ConfigurationError, OperationFailure
from pymongo.message import _maybe_add_read_preference

View File

@ -24,7 +24,7 @@ from pathlib import Path
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
from test.utils_shared import OvertCommandListener
from pymongo import DESCENDING
@ -42,11 +42,7 @@ from pymongo.write_concern import WriteConcern
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "read_write_concern")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "read_write_concern")
TEST_PATH = get_test_path("read_write_concern")
class TestReadWriteConcernSpec(AsyncIntegrationTest):

View File

@ -22,21 +22,15 @@ from pathlib import Path
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "retryable_reads/unified")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "retryable_reads/unified")
# Generate unified tests.
# PyMongo does not support MapReduce, ListDatabaseObjects or ListCollectionObjects.
globals().update(
generate_test_classes(
TEST_PATH,
get_test_path("retryable_reads", "unified"),
module=__name__,
expected_failures=["ListDatabaseObjects .*", "ListCollectionObjects .*", "MapReduce .*"],
)

View File

@ -22,18 +22,14 @@ from pathlib import Path
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "retryable_writes/unified")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "retryable_writes/unified")
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
globals().update(
generate_test_classes(get_test_path("retryable_writes", "unified"), module=__name__)
)
if __name__ == "__main__":
unittest.main()

View File

@ -18,20 +18,13 @@ from __future__ import annotations
import os
import unittest
from pathlib import Path
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "run_command")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "run_command")
globals().update(
generate_test_classes(
os.path.join(TEST_PATH, "unified"),
get_test_path("run_command", "unified"),
module=__name__,
)
)

View File

@ -17,9 +17,10 @@ from __future__ import annotations
import os
import sys
import time
from pathlib import Path
from pymongo import AsyncMongoClient, ReadPreference
from pymongo import AsyncMongoClient, ReadPreference, monitoring
from pymongo.asynchronous.settings import TopologySettings
from pymongo.asynchronous.topology import Topology
from pymongo.errors import ServerSelectionTimeoutError
@ -30,7 +31,7 @@ from pymongo.typings import strip_optional
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
from test.asynchronous import AsyncIntegrationTest, async_client_context, client_knobs, unittest
from test.asynchronous.utils import async_wait_until
from test.asynchronous.utils_selection_tests import (
create_selection_tests,
@ -42,6 +43,7 @@ from test.utils_selection_tests_shared import (
)
from test.utils_shared import (
FunctionCallRecorder,
HeartbeatEventListener,
OvertCommandListener,
)
@ -207,6 +209,40 @@ class TestCustomServerSelectorFunction(AsyncIntegrationTest):
)
self.assertEqual(selector.call_count, 0)
@async_client_context.require_replica_set
@async_client_context.require_failCommand_appName
async def test_server_selection_getMore_blocks(self):
hb_listener = HeartbeatEventListener()
client = await self.async_rs_client(
event_listeners=[hb_listener], heartbeatFrequencyMS=500, appName="heartbeatFailedClient"
)
coll = client.db.test
await coll.drop()
docs = [{"x": 1} for _ in range(5)]
await coll.insert_many(docs)
fail_heartbeat = {
"configureFailPoint": "failCommand",
"mode": {"times": 4},
"data": {
"failCommands": [HelloCompat.LEGACY_CMD, "hello"],
"closeConnection": True,
"appName": "heartbeatFailedClient",
},
}
def hb_failed(event):
return isinstance(event, monitoring.ServerHeartbeatFailedEvent)
cursor = coll.find({}, batch_size=1)
await cursor.next() # force initial query that will pin the address for the getMore
async with self.fail_point(fail_heartbeat):
await async_wait_until(
lambda: hb_listener.matching(hb_failed), "published failed event"
)
self.assertEqual(len(await cursor.to_list()), 4)
if __name__ == "__main__":
unittest.main()

View File

@ -22,20 +22,14 @@ from pathlib import Path
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "server_selection_logging")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "server_selection_logging")
globals().update(
generate_test_classes(
TEST_PATH,
get_test_path("server_selection_logging"),
module=__name__,
)
)

View File

@ -48,6 +48,7 @@ from gridfs.asynchronous.grid_file import AsyncGridFS, AsyncGridFSBucket
from pymongo import ASCENDING, AsyncMongoClient, _csot, monitoring
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.cursor import AsyncCursor
from pymongo.asynchronous.helpers import anext
from pymongo.common import _MAX_END_SESSIONS
from pymongo.errors import ConfigurationError, InvalidOperation, OperationFailure
from pymongo.operations import IndexModel, InsertOne, UpdateOne

View File

@ -22,19 +22,12 @@ from pathlib import Path
sys.path[0:0] = [""]
from test import unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "sessions")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "sessions")
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
globals().update(generate_test_classes(get_test_path("sessions"), module=__name__))
if __name__ == "__main__":
unittest.main()

View File

@ -20,7 +20,6 @@ import random
import sys
import time
from io import BytesIO
from test.asynchronous.utils_spec_runner import AsyncSpecRunner
from gridfs.asynchronous.grid_file import AsyncGridFS, AsyncGridFSBucket
from pymongo.asynchronous.pool import PoolState
@ -42,6 +41,7 @@ from pymongo.asynchronous import client_session
from pymongo.asynchronous.client_session import TransactionOptions
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.cursor import AsyncCursor
from pymongo.asynchronous.helpers import anext
from pymongo.errors import (
AutoReconnect,
CollectionInvalid,
@ -63,15 +63,8 @@ _IS_SYNC = False
UNPIN_TEST_MAX_ATTEMPTS = 50
class AsyncTransactionsBase(AsyncSpecRunner):
def maybe_skip_scenario(self, test):
super().maybe_skip_scenario(test)
if (
"secondary" in self.id()
and not async_client_context.is_mongos
and not async_client_context.has_secondaries
):
raise unittest.SkipTest("No secondaries")
class AsyncTransactionsBase(AsyncIntegrationTest):
pass
class TestTransactions(AsyncTransactionsBase):

View File

@ -22,7 +22,7 @@ from pathlib import Path
sys.path[0:0] = [""]
from test import client_context, unittest
from test.asynchronous.unified_format import generate_test_classes
from test.asynchronous.unified_format import generate_test_classes, get_test_path
_IS_SYNC = False
@ -31,25 +31,13 @@ def setUpModule():
pass
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "transactions/unified")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "transactions/unified")
# Generate unified tests.
globals().update(generate_test_classes(get_test_path("transactions/unified"), module=__name__))
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
# Location of JSON test specifications for transactions-convenient-api.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "transactions-convenient-api/unified")
else:
TEST_PATH = os.path.join(
Path(__file__).resolve().parent.parent, "transactions-convenient-api/unified"
)
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
globals().update(
generate_test_classes(get_test_path("transactions-convenient-api/unified"), module=__name__)
)
if __name__ == "__main__":
unittest.main()

View File

@ -21,18 +21,18 @@ from typing import Any
sys.path[0:0] = [""]
from test import UnitTest, unittest
from test.asynchronous.unified_format import MatchEvaluatorUtil, generate_test_classes
from test.asynchronous.unified_format import (
MatchEvaluatorUtil,
generate_test_classes,
get_test_path,
)
from bson import ObjectId
_IS_SYNC = False
# Location of JSON test specifications.
if _IS_SYNC:
TEST_PATH = os.path.join(Path(__file__).resolve().parent, "unified-test-format")
else:
TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "unified-test-format")
TEST_PATH = get_test_path("unified-test-format")
globals().update(
generate_test_classes(

Some files were not shown because too many files have changed in this diff Show More