Merge branch 'master' of github.com:mongodb/mongo-python-driver into backpressure
This commit is contained in:
commit
84699d284b
@ -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
@ -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]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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")
|
||||
|
||||
31
.evergreen/spec-patch/PYTHON-5517.patch
Normal file
31
.evergreen/spec-patch/PYTHON-5517.patch
Normal 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"
|
||||
}
|
||||
@ -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",
|
||||
27
.github/pull_request_template.md
vendored
27
.github/pull_request_template.md
vendored
@ -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 didn’t 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. -->
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -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}}"
|
||||
|
||||
15
.github/workflows/dist.yml
vendored
15
.github/workflows/dist.yml
vendored
@ -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: "./*"
|
||||
|
||||
2
.github/workflows/release-python.yml
vendored
2
.github/workflows/release-python.yml
vendored
@ -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/
|
||||
|
||||
48
.github/workflows/sbom.yml
vendored
48
.github/workflows/sbom.yml
vendored
@ -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
|
||||
|
||||
44
.github/workflows/test-python.yml
vendored
44
.github/workflows/test-python.yml
vendored
@ -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:
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
208
bson/binary.py
208
bson/binary.py
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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}')"
|
||||
|
||||
@ -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)
|
||||
--------------------------------------
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
18
justfile
18
justfile
@ -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
|
||||
|
||||
@ -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], ...]:
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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__()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
pykerberos;os.name!='nt'
|
||||
pykerberos>=1.2.4;os.name!='nt'
|
||||
winkerberos>=0.5.0;os.name=='nt'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1 +1 @@
|
||||
python-snappy
|
||||
python-snappy>=0.6.0
|
||||
|
||||
307
sbom.json
307
sbom.json
@ -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"
|
||||
}
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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] = [""]
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 .*"],
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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__,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user