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

This commit is contained in:
Steven Silvester 2025-09-25 13:11:09 -05:00
commit b830ef02d3
No known key found for this signature in database
30 changed files with 1541 additions and 1431 deletions

View File

@ -151,6 +151,7 @@ functions:
- VERSION
- IS_WIN32
- REQUIRE_FIPS
- TEST_MIN_DEPS
type: test
- command: subprocess.exec
params:

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,5 @@
buildvariants:
# Alternative hosts tests
- name: openssl-1.0.2-rhel7-v5.0-python3.9
tasks:
- name: .test-no-toolchain
display_name: OpenSSL 1.0.2 RHEL7 v5.0 Python3.9
run_on:
- rhel79-small
batchtime: 1440
expansions:
VERSION: "5.0"
PYTHON_VERSION: "3.9"
PYTHON_BINARY: /opt/python/3.9/bin/python3
- name: other-hosts-rhel9-fips-latest
tasks:
- name: .test-no-toolchain
@ -153,17 +142,16 @@ buildvariants:
- rhel87-small
# Disable test commands tests
- name: disable-test-commands-rhel8-python3.9
- name: disable-test-commands-rhel8
tasks:
- name: .test-standard .server-latest
display_name: Disable test commands RHEL8 Python3.9
display_name: Disable test commands RHEL8
run_on:
- rhel87-small
expansions:
AUTH: auth
SSL: ssl
DISABLE_TEST_COMMANDS: "1"
PYTHON_BINARY: /opt/python/3.9/bin/python3
# Doctests tests
- name: doctests-rhel8
@ -179,6 +167,7 @@ buildvariants:
- name: encryption-rhel8
tasks:
- name: .test-non-standard
- name: .test-min-deps
display_name: Encryption RHEL8
run_on:
- rhel87-small
@ -209,6 +198,7 @@ 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
@ -500,14 +490,14 @@ buildvariants:
SUB_TEST_NAME: pyopenssl
# Search index tests
- name: search-index-helpers-rhel8-python3.9
- name: search-index-helpers-rhel8-python3.10
tasks:
- name: .search_index
display_name: Search Index Helpers RHEL8 Python3.9
display_name: Search Index Helpers RHEL8 Python3.10
run_on:
- rhel87-small
expansions:
PYTHON_BINARY: /opt/python/3.9/bin/python3
PYTHON_BINARY: /opt/python/3.10/bin/python3
# Server version tests
- name: mongodb-v4.2

View File

@ -20,8 +20,13 @@ fi
set -o xtrace
# Install python with pip.
PYTHON_VER="python3.9"
PYTHON_VER="python3.10"
apt-get -qq update < /dev/null > /dev/null
apt-get -q install -y software-properties-common
# Use openpgp to avoid gpg key timeout.
mkdir -p $HOME/.gnupg
echo "keyserver keys.openpgp.org" >> $HOME/.gnupg/gpg.conf
add-apt-repository -y 'ppa:deadsnakes/ppa'
apt-get -qq install $PYTHON_VER $PYTHON_VER-venv build-essential $PYTHON_VER-dev -y < /dev/null > /dev/null
export PYTHON_BINARY=$PYTHON_VER

View File

@ -6,7 +6,8 @@ SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
SCRIPT_DIR="$( cd -- "$SCRIPT_DIR" > /dev/null 2>&1 && pwd )"
ROOT_DIR="$(dirname $SCRIPT_DIR)"
pushd $ROOT_DIR
PREV_DIR=$(pwd)
cd $ROOT_DIR
# Try to source the env file.
if [ -f $SCRIPT_DIR/scripts/env.sh ]; then
@ -25,7 +26,17 @@ else
exit 1
fi
# Start the test runner.
uv run ${UV_ARGS} --reinstall .evergreen/scripts/run_tests.py "$@"
cleanup_tests() {
# Avoid leaving the lock file in a changed state when we change the resolution type.
if [ -n "${TEST_MIN_DEPS:-}" ]; then
git checkout uv.lock || true
fi
cd $PREV_DIR
}
popd
trap "cleanup_tests" SIGINT ERR
# Start the test runner.
uv run ${UV_ARGS} --reinstall-package pymongo .evergreen/scripts/run_tests.py "$@"
cleanup_tests

View File

@ -143,7 +143,7 @@ def create_encryption_variants() -> list[BuildVariant]:
):
expansions = get_encryption_expansions(encryption)
display_name = get_variant_name(encryption, host, **expansions)
tasks = [".test-non-standard"]
tasks = [".test-non-standard", ".test-min-deps"]
if host != "rhel8":
tasks = [".test-non-standard !.pypy"]
variant = create_variant(
@ -330,10 +330,9 @@ def create_mod_wsgi_variants():
def create_disable_test_commands_variants():
host = DEFAULT_HOST
expansions = dict(AUTH="auth", SSL="ssl", DISABLE_TEST_COMMANDS="1")
python = CPYTHONS[0]
display_name = get_variant_name("Disable test commands", host, python=python)
display_name = get_variant_name("Disable test commands", host)
tasks = [".test-standard .server-latest"]
return [create_variant(tasks, display_name, host=host, python=python, expansions=expansions)]
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
def create_oidc_auth_variants():
@ -480,19 +479,6 @@ def create_alternative_hosts_variants():
batchtime = BATCHTIME_DAY
variants = []
host = HOSTS["rhel7"]
version = "5.0"
variants.append(
create_variant(
[".test-no-toolchain"],
get_variant_name("OpenSSL 1.0.2", host, python=CPYTHONS[0], version=version),
host=host,
python=CPYTHONS[0],
batchtime=batchtime,
expansions=dict(VERSION=version, PYTHON_VERSION=CPYTHONS[0]),
)
)
version = "latest"
for host_name in OTHER_HOSTS:
expansions = dict(VERSION="latest")
@ -528,22 +514,20 @@ def create_aws_lambda_variants():
def create_server_version_tasks():
tasks = []
task_inputs = []
task_combos = set()
# All combinations of topology, auth, ssl, and sync should be tested.
for (topology, auth, ssl, sync), python in zip_cycle(
list(product(TOPOLOGIES, ["auth", "noauth"], ["ssl", "nossl"], SYNCS)), ALL_PYTHONS
):
task_inputs.append((topology, auth, ssl, sync, python))
task_combos.add((topology, auth, ssl, sync, python))
# Every python should be tested with sharded cluster, auth, ssl, with sync and async.
for python, sync in product(ALL_PYTHONS, SYNCS):
task_input = ("sharded_cluster", "auth", "ssl", sync, python)
if task_input not in task_inputs:
task_inputs.append(task_input)
task_combos.add(("sharded_cluster", "auth", "ssl", sync, python))
# Assemble the tasks.
seen = set()
for topology, auth, ssl, sync, python in task_inputs:
for topology, auth, ssl, sync, python in sorted(task_combos):
combo = f"{topology}-{auth}-{ssl}"
tags = ["server-version", f"python-{python}", combo, sync]
if combo in [
@ -558,7 +542,12 @@ def create_server_version_tasks():
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
if python not in PYPYS:
expansions["COVERAGE"] = "1"
name = get_task_name("test-server-version", python=python, sync=sync, **expansions)
name = get_task_name(
"test-server-version",
python=python,
sync=sync,
**expansions,
)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
test_vars["PYTHON_VERSION"] = python
@ -590,15 +579,15 @@ def create_no_toolchain_tasks():
def create_test_non_standard_tasks():
"""For variants that set a TEST_NAME."""
tasks = []
task_combos = []
task_combos = set()
# For each version and topology, rotate through the CPythons.
for (version, topology), python in zip_cycle(list(product(ALL_VERSIONS, TOPOLOGIES)), CPYTHONS):
pr = version == "latest"
task_combos.append((version, topology, python, pr))
task_combos.add((version, topology, python, pr))
# For each PyPy and topology, rotate through the the versions.
for (python, topology), version in zip_cycle(list(product(PYPYS, TOPOLOGIES)), ALL_VERSIONS):
task_combos.append((version, topology, python, False))
for version, topology, python, pr in task_combos:
task_combos.add((version, topology, python, False))
for version, topology, python, pr in sorted(task_combos):
auth, ssl = get_standard_auth_ssl(topology)
tags = [
"test-non-standard",
@ -621,6 +610,22 @@ def create_test_non_standard_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 = []
@ -794,9 +799,12 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
tags.append("pr")
task_name = get_task_name(
f"test-ocsp-{algo}-{base_task_name}", python=python, version=version
f"test-ocsp-{algo}-{base_task_name}",
python=python,
version=version,
)
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
return tasks
@ -1075,6 +1083,7 @@ def create_run_tests_func():
"VERSION",
"IS_WIN32",
"REQUIRE_FIPS",
"TEST_MIN_DEPS",
]
args = [".evergreen/just.sh", "setup-tests", "${TEST_NAME}", "${SUB_TEST_NAME}"]
setup_cmd = get_subprocess_exec(include_expansions_in_env=includes, args=args)

View File

@ -22,7 +22,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.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
CPYTHONS = ["3.10", "3.9", "3.11", "3.12", "3.13", "3.14"]
PYPYS = ["pypy3.10"]
ALL_PYTHONS = CPYTHONS + PYPYS
MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]]
@ -42,6 +42,7 @@ DISPLAY_LOOKUP = dict(
sync={"sync": "Sync", "async": "Async"},
coverage={"1": "cov"},
no_ext={"1": "No C"},
test_min_deps={True: "Min Deps"},
)
HOSTS = dict()
@ -202,7 +203,7 @@ def get_common_name(base: str, sep: str, **kwargs) -> str:
name = f"Python{value}"
else:
name = f"PyPy{value.replace('pypy', '')}"
elif key.lower() in DISPLAY_LOOKUP:
elif key.lower() in DISPLAY_LOOKUP and value in DISPLAY_LOOKUP[key.lower()]:
name = DISPLAY_LOOKUP[key.lower()][value]
else:
continue

View File

@ -33,7 +33,7 @@ def _setup_azure_vm(base_env: dict[str, str]) -> None:
env["AZUREKMS_CMD"] = "sudo apt-get install -y python3-dev build-essential"
run_command(f"{azure_dir}/run-command.sh", env=env)
env["AZUREKMS_CMD"] = "bash .evergreen/just.sh setup-tests kms azure-remote"
env["AZUREKMS_CMD"] = "NO_EXT=1 bash .evergreen/just.sh setup-tests kms azure-remote"
run_command(f"{azure_dir}/run-command.sh", env=env)
LOGGER.info("Setting up Azure VM... done.")
@ -53,7 +53,7 @@ def _setup_gcp_vm(base_env: dict[str, str]) -> None:
env["GCPKMS_CMD"] = "sudo apt-get install -y python3-dev build-essential"
run_command(f"{gcp_dir}/run-command.sh", env=env)
env["GCPKMS_CMD"] = "bash ./.evergreen/just.sh setup-tests kms gcp-remote"
env["GCPKMS_CMD"] = "NO_EXT=1 bash ./.evergreen/just.sh setup-tests kms gcp-remote"
run_command(f"{gcp_dir}/run-command.sh", env=env)
LOGGER.info("Setting up GCP VM...")
@ -98,6 +98,13 @@ def setup_kms(sub_test_name: str) -> None:
if sub_test_target == "azure":
os.environ["AZUREKMS_VMNAME_PREFIX"] = "PYTHON_DRIVER"
# Found using "az vm image list --output table"
os.environ[
"AZUREKMS_IMAGE"
] = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest"
else:
os.environ["GCPKMS_IMAGEFAMILY"] = "debian-12"
run_command("./setup.sh", cwd=kms_dir)
base_env = _load_kms_config(sub_test_target)

View File

@ -42,6 +42,11 @@ def setup_oidc(sub_test_name: str) -> dict[str, str] | None:
if sub_test_name == "azure":
env["AZUREOIDC_VMNAME_PREFIX"] = "PYTHON_DRIVER"
if "-remote" not in sub_test_name:
if sub_test_name == "azure":
# Found using "az vm image list --output table"
env["AZUREOIDC_IMAGE"] = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest"
else:
env["GCPKMS_IMAGEFAMILY"] = "debian-12"
run_command(f"bash {target_dir}/setup.sh", env=env)
if sub_test_name in K8S_NAMES:
run_command(f"bash {target_dir}/setup-pod.sh {sub_test_name}")
@ -84,7 +89,7 @@ def test_oidc_send_to_remote(sub_test_name: str) -> None:
env[f"{upper_name}OIDC_DRIVERS_TAR_FILE"] = TMP_DRIVER_FILE
env[
f"{upper_name}OIDC_TEST_CMD"
] = f"OIDC_ENV={sub_test_name} ./.evergreen/run-mongodb-oidc-test.sh"
] = f"NO_EXT=1 OIDC_ENV={sub_test_name} ./.evergreen/run-mongodb-oidc-test.sh"
elif sub_test_name in K8S_NAMES:
env["K8S_DRIVERS_TAR_FILE"] = TMP_DRIVER_FILE
env["K8S_TEST_CMD"] = "OIDC_ENV=k8s ./.evergreen/run-mongodb-oidc-test.sh"

View File

@ -30,13 +30,14 @@ SUB_TEST_NAME = os.environ.get("SUB_TEST_NAME")
def list_packages():
packages = dict()
packages = set()
for distribution in importlib_metadata.distributions():
packages[distribution.name] = distribution
if distribution.name:
packages.add(distribution.name)
print("Package Version URL")
print("------------------- ----------- ----------------------------------------------------")
for name in sorted(packages):
distribution = packages[name]
distribution = importlib_metadata.distribution(name)
url = ""
if distribution.origin is not None:
url = distribution.origin.url
@ -136,6 +137,9 @@ def handle_aws_lambda() -> None:
def run() -> None:
# Add diagnostic for python version.
print("Running with python", sys.version)
# List the installed packages.
list_packages()

View File

@ -53,7 +53,7 @@ EXTRAS_MAP = {
GROUP_MAP = dict(mockupdb="mockupdb", perf="perf")
# The python version used for perf tests.
PERF_PYTHON_VERSION = "3.9.13"
PERF_PYTHON_VERSION = "3.10.11"
def is_set(var: str) -> bool:
@ -90,6 +90,13 @@ def setup_libmongocrypt():
distro = get_distro()
if distro.name.startswith("Debian"):
target = f"debian{distro.version_id}"
elif distro.name.startswith("Ubuntu"):
if distro.version_id == "20.04":
target = "debian11"
elif distro.version_id == "22.04":
target = "debian12"
elif distro.version_id == "24.04":
target = "debian13"
elif distro.name.startswith("Red Hat"):
if distro.version_id.startswith("7"):
target = "rhel-70-64-bit"
@ -160,7 +167,6 @@ def handle_test_env() -> None:
write_env("PIP_QUIET") # Quiet by default.
write_env("PIP_PREFER_BINARY") # Prefer binary dists by default.
write_env("UV_FROZEN") # Do not modify lock files.
# Set an environment variable for the test name and sub test name.
write_env(f"TEST_{test_name.upper()}")
@ -178,6 +184,9 @@ def handle_test_env() -> None:
if group := GROUP_MAP.get(test_name, ""):
UV_ARGS.append(f"--group {group}")
if opts.test_min_deps:
UV_ARGS.append("--resolution=lowest-direct")
if test_name == "auth_oidc":
from oidc_tester import setup_oidc
@ -233,7 +242,7 @@ def handle_test_env() -> None:
if is_set("MONGODB_URI"):
write_env("PYMONGO_MUST_CONNECT", "true")
if is_set("DISABLE_TEST_COMMANDS") or opts.disable_test_commands:
if opts.disable_test_commands:
write_env("PYMONGO_DISABLE_TEST_COMMANDS", "1")
if test_name == "enterprise_auth":
@ -345,10 +354,10 @@ def handle_test_env() -> None:
if not (ROOT / "libmongocrypt").exists():
setup_libmongocrypt()
# TODO: Test with 'pip install pymongocrypt'
UV_ARGS.append(
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
)
if not opts.test_min_deps:
UV_ARGS.append(
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
)
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
BASE = ROOT / "libmongocrypt/nocrypto"
@ -377,7 +386,7 @@ def handle_test_env() -> None:
if sub_test_name == "pyopenssl":
UV_ARGS.append("--extra ocsp")
if is_set("TEST_CRYPT_SHARED") or opts.crypt_shared:
if opts.crypt_shared:
config = read_env(f"{DRIVERS_TOOLS}/mo-expansion.sh")
CRYPT_SHARED_DIR = Path(config["CRYPT_SHARED_LIB_PATH"]).parent.as_posix()
LOGGER.info("Using crypt_shared_dir %s", CRYPT_SHARED_DIR)
@ -447,14 +456,14 @@ def handle_test_env() -> None:
# Add coverage if requested.
# Only cover CPython. PyPy reports suspiciously low coverage.
if (is_set("COVERAGE") or opts.cov) and platform.python_implementation() == "CPython":
if opts.cov and platform.python_implementation() == "CPython":
# Keep in sync with combine-coverage.sh.
# coverage >=5 is needed for relative_files=true.
UV_ARGS.append("--group coverage")
TEST_ARGS = f"{TEST_ARGS} --cov"
write_env("COVERAGE")
if is_set("GREEN_FRAMEWORK") or opts.green_framework:
if opts.green_framework:
framework = opts.green_framework or os.environ["GREEN_FRAMEWORK"]
UV_ARGS.append(f"--group {framework}")

View File

@ -60,6 +60,9 @@ NO_RUN_ORCHESTRATION = [
"ocsp",
]
# Mapping of env variables to options
OPTION_TO_ENV_VAR = {"cov": "COVERAGE", "crypt_shared": "TEST_CRYPT_SHARED"}
def get_test_options(
description, require_sub_test_name=True, allow_extra_opts=False
@ -94,6 +97,9 @@ def get_test_options(
)
parser.add_argument("--auth", action="store_true", help="Whether to add authentication.")
parser.add_argument("--ssl", action="store_true", help="Whether to add TLS configuration.")
parser.add_argument(
"--test-min-deps", action="store_true", help="Test against minimum dependency versions"
)
# Add the test modifiers.
if require_sub_test_name:
@ -127,26 +133,53 @@ def get_test_options(
opts, extra_opts = parser.parse_args(), []
else:
opts, extra_opts = parser.parse_known_args()
if opts.verbose:
LOGGER.setLevel(logging.DEBUG)
elif opts.quiet:
LOGGER.setLevel(logging.WARNING)
# Convert list inputs to strings.
for name in vars(opts):
value = getattr(opts, name)
if isinstance(value, list):
setattr(opts, name, value[0])
# Handle validation and environment variable overrides.
test_name = opts.test_name
sub_test_name = opts.sub_test_name if require_sub_test_name else ""
if require_sub_test_name and test_name in SUB_TEST_REQUIRED and not sub_test_name:
raise ValueError(f"Test '{test_name}' requires a sub_test_name")
if "auth" in test_name or os.environ.get("AUTH") == "auth":
handle_env_overrides(parser, opts)
if "auth" in test_name:
opts.auth = True
# 'auth_aws ecs' shouldn't have extra auth set.
if test_name == "auth_aws" and sub_test_name == "ecs":
opts.auth = False
if os.environ.get("SSL") == "ssl":
opts.ssl = True
if opts.verbose:
LOGGER.setLevel(logging.DEBUG)
elif opts.quiet:
LOGGER.setLevel(logging.WARNING)
return opts, extra_opts
def handle_env_overrides(parser: argparse.ArgumentParser, opts: argparse.Namespace) -> None:
# Get the options, and then allow environment variable overrides.
for key in vars(opts):
if key in OPTION_TO_ENV_VAR:
env_var = OPTION_TO_ENV_VAR[key]
else:
env_var = key.upper()
if env_var in os.environ:
if parser.get_default(key) != getattr(opts, key):
LOGGER.info("Overriding env var '%s' with cli option", env_var)
elif env_var == "AUTH":
opts.auth = os.environ.get("AUTH") == "auth"
elif env_var == "SSL":
ssl_opt = os.environ.get("SSL", "")
opts.ssl = ssl_opt and ssl_opt.lower() != "nossl"
elif isinstance(getattr(opts, key), bool):
if os.environ[env_var]:
setattr(opts, key, True)
else:
setattr(opts, key, os.environ[env_var])
def read_env(path: Path | str) -> dict[str, str]:
config = dict()
with Path(path).open() as fid:

View File

@ -225,7 +225,7 @@ jobs:
permissions:
contents: read
runs-on: ubuntu-latest
name: Test using minimum dependencies and supported Python
name: Test minimum dependencies and Python
steps:
- uses: actions/checkout@v5
with:
@ -238,37 +238,10 @@ jobs:
uses: mongodb-labs/drivers-evergreen-tools@master
with:
version: "8.0"
# Async and our test_dns do not support dnspython 1.X, so we don't run async or dns tests here
- name: Run tests
shell: bash
run: |
uv venv
source .venv/bin/activate
uv pip install -e ".[test]" --resolution=lowest-direct
pytest -v test/test_srv_polling.py
test_minimum_for_async:
permissions:
contents: read
runs-on: ubuntu-latest
name: Test async's minimum dependencies and Python
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v5
with:
python-version: '3.9'
- id: setup-mongodb
uses: mongodb-labs/drivers-evergreen-tools@master
with:
version: "8.0"
# The lifetime kwarg we use in srv resolution was added to the async resolver API in dnspython 2.1.0
- name: Run tests
shell: bash
run: |
uv venv
source .venv/bin/activate
uv pip install -e ".[test]" --resolution=lowest-direct dnspython==2.1.0 --force-reinstall
uv pip install -e ".[test]" --resolution=lowest-direct --force-reinstall
pytest -v test/test_srv_polling.py test/test_dns.py test/asynchronous/test_srv_polling.py test/asynchronous/test_dns.py

View File

@ -382,6 +382,11 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
- Finally, you can use `just setup-tests --debug-log`.
- For evergreen patch builds, you can use `evergreen patch --param DEBUG_LOG=1` to enable debug logs for failed tests in the patch.
## Testing minimum dependencies
To run any of the test suites with minimum supported dependencies, pass `--test-min-deps` to
`just setup-tests`.
## 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

View File

@ -9,6 +9,8 @@ PyMongo 4.16 brings a number of changes including:
- 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>`_.
- Removed support for Eventlet.
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.

View File

@ -58,20 +58,11 @@ async def _resolve(*args: Any, **kwargs: Any) -> resolver.Answer:
if _IS_SYNC:
from dns import resolver
if hasattr(resolver, "resolve"):
# dnspython >= 2
return resolver.resolve(*args, **kwargs)
# dnspython 1.X
return resolver.query(*args, **kwargs)
return resolver.resolve(*args, **kwargs)
else:
from dns import asyncresolver
if hasattr(asyncresolver, "resolve"):
# dnspython >= 2
return await asyncresolver.resolve(*args, **kwargs) # type:ignore[return-value]
raise ConfigurationError(
"Upgrade to dnspython version >= 2.0 to use AsyncMongoClient with mongodb+srv:// connections."
)
return await asyncresolver.resolve(*args, **kwargs) # type:ignore[return-value]
_INVALID_HOST_MSG = (

View File

@ -58,20 +58,11 @@ def _resolve(*args: Any, **kwargs: Any) -> resolver.Answer:
if _IS_SYNC:
from dns import resolver
if hasattr(resolver, "resolve"):
# dnspython >= 2
return resolver.resolve(*args, **kwargs)
# dnspython 1.X
return resolver.query(*args, **kwargs)
return resolver.resolve(*args, **kwargs)
else:
from dns import asyncresolver
if hasattr(asyncresolver, "resolve"):
# dnspython >= 2
return asyncresolver.resolve(*args, **kwargs) # type:ignore[return-value]
raise ConfigurationError(
"Upgrade to dnspython version >= 2.0 to use MongoClient with mongodb+srv:// connections."
)
return asyncresolver.resolve(*args, **kwargs) # type:ignore[return-value]
_INVALID_HOST_MSG = (

View File

@ -48,8 +48,7 @@ Tracker = "https://jira.mongodb.org/projects/PYTHON/issues"
[dependency-groups]
dev = []
pip = ["pip"]
# TODO: PYTHON-5464
gevent = ["gevent", "cffi>=2.0.0b1;python_version=='3.14'"]
gevent = ["gevent>=20.6.0"]
coverage = [
"pytest-cov",
"coverage>=5,<=7.10.6"
@ -57,7 +56,7 @@ coverage = [
mockupdb = [
"mockupdb@git+https://github.com/mongodb-labs/mongo-mockup-db@master"
]
perf = ["simplejson"]
perf = ["simplejson>=3.17.0"]
typing = [
"mypy==1.18.1",
"pyright==1.1.405",

View File

@ -1 +1 @@
dnspython>=1.16.0,<3.0.0
dnspython>=2.6.1,<3.0.0

View File

@ -1,3 +1,3 @@
pymongo-auth-aws>=1.1.0,<2.0.0
pymongocrypt>=1.13.0,<2.0.0
certifi;os.name=='nt' or sys_platform=='darwin'
certifi>=2023.7.22;os.name=='nt' or sys_platform=='darwin'

View File

@ -5,7 +5,7 @@
# Fallback to certifi on Windows if we can't load CA certs from the system
# store and just use certifi on macOS.
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
certifi;os.name=='nt' or sys_platform=='darwin'
certifi>=2023.7.22;os.name=='nt' or sys_platform=='darwin'
pyopenssl>=17.2.0
requests<3.0.0
cryptography>=2.5

View File

@ -519,6 +519,19 @@ class ClientContext:
"Libmongocrypt version must be at least %s" % str(other_version),
)
def require_pymongocrypt_min(self, *ver):
other_version = Version(*ver)
if not _HAVE_PYMONGOCRYPT:
version = Version.from_string("0.0.0")
else:
from pymongocrypt import __version__ as pymongocrypt_version
version = Version.from_string(pymongocrypt_version)
return self._require(
lambda: version >= other_version,
"PyMongoCrypt version must be at least %s" % str(other_version),
)
def require_auth(self, func):
"""Run a test only if the server is running with auth enabled."""
return self._require(

View File

@ -519,6 +519,19 @@ class AsyncClientContext:
"Libmongocrypt version must be at least %s" % str(other_version),
)
def require_pymongocrypt_min(self, *ver):
other_version = Version(*ver)
if not _HAVE_PYMONGOCRYPT:
version = Version.from_string("0.0.0")
else:
from pymongocrypt import __version__ as pymongocrypt_version
version = Version.from_string(pymongocrypt_version)
return self._require(
lambda: version >= other_version,
"PyMongoCrypt version must be at least %s" % str(other_version),
)
def require_auth(self, func):
"""Run a test only if the server is running with auth enabled."""
return self._require(

View File

@ -2059,7 +2059,7 @@ class TestClient(AsyncIntegrationTest):
async def test_handshake_01_aws(self):
await self._test_handshake(
{
"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9",
"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10",
"AWS_REGION": "us-east-2",
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024",
},
@ -2097,7 +2097,7 @@ class TestClient(AsyncIntegrationTest):
async def test_handshake_05_multiple(self):
await self._test_handshake(
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "FUNCTIONS_WORKER_RUNTIME": "python"},
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10", "FUNCTIONS_WORKER_RUNTIME": "python"},
None,
)
# Extra cases for other combos.
@ -2109,13 +2109,16 @@ class TestClient(AsyncIntegrationTest):
async def test_handshake_06_region_too_long(self):
await self._test_handshake(
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_REGION": "a" * 512},
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10", "AWS_REGION": "a" * 512},
{"name": "aws.lambda"},
)
async def test_handshake_07_memory_invalid_int(self):
await self._test_handshake(
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big"},
{
"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10",
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big",
},
{"name": "aws.lambda"},
)

View File

@ -485,7 +485,7 @@ class TestServerMonitoringMode(AsyncIntegrationTest):
async def test_rtt_connection_is_disabled_auto(self):
envs = [
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9"},
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10"},
{"FUNCTIONS_WORKER_RUNTIME": "python"},
{"K_SERVICE": "gcpservicename"},
{"FUNCTION_NAME": "gcpfunctionname"},

View File

@ -3448,6 +3448,7 @@ class TestExplicitTextEncryptionProse(AsyncEncryptionIntegrationTest):
@async_client_context.require_no_standalone
@async_client_context.require_version_min(8, 2, -1)
@async_client_context.require_libmongocrypt_min(1, 15, 1)
@async_client_context.require_pymongocrypt_min(1, 16, 0)
async def asyncSetUp(self):
await super().asyncSetUp()
# Load the file key1-document.json as key1Document.

View File

@ -2016,7 +2016,7 @@ class TestClient(IntegrationTest):
def test_handshake_01_aws(self):
self._test_handshake(
{
"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9",
"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10",
"AWS_REGION": "us-east-2",
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024",
},
@ -2054,7 +2054,7 @@ class TestClient(IntegrationTest):
def test_handshake_05_multiple(self):
self._test_handshake(
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "FUNCTIONS_WORKER_RUNTIME": "python"},
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10", "FUNCTIONS_WORKER_RUNTIME": "python"},
None,
)
# Extra cases for other combos.
@ -2066,13 +2066,16 @@ class TestClient(IntegrationTest):
def test_handshake_06_region_too_long(self):
self._test_handshake(
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_REGION": "a" * 512},
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10", "AWS_REGION": "a" * 512},
{"name": "aws.lambda"},
)
def test_handshake_07_memory_invalid_int(self):
self._test_handshake(
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big"},
{
"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10",
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big",
},
{"name": "aws.lambda"},
)

View File

@ -483,7 +483,7 @@ class TestServerMonitoringMode(IntegrationTest):
def test_rtt_connection_is_disabled_auto(self):
envs = [
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.9"},
{"AWS_EXECUTION_ENV": "AWS_Lambda_python3.10"},
{"FUNCTIONS_WORKER_RUNTIME": "python"},
{"K_SERVICE": "gcpservicename"},
{"FUNCTION_NAME": "gcpfunctionname"},

View File

@ -3430,6 +3430,7 @@ class TestExplicitTextEncryptionProse(EncryptionIntegrationTest):
@client_context.require_no_standalone
@client_context.require_version_min(8, 2, -1)
@client_context.require_libmongocrypt_min(1, 15, 1)
@client_context.require_pymongocrypt_min(1, 16, 0)
def setUp(self):
super().setUp()
# Load the file key1-document.json as key1Document.

14
uv.lock generated
View File

@ -1201,7 +1201,6 @@ coverage = [
{ name = "pytest-cov" },
]
gevent = [
{ name = "cffi", version = "2.0.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.14.*'" },
{ name = "gevent" },
]
mockupdb = [
@ -1222,10 +1221,10 @@ typing = [
[package.metadata]
requires-dist = [
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'encryption') or (sys_platform == 'darwin' and extra == 'encryption')" },
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'ocsp') or (sys_platform == 'darwin' and extra == 'ocsp')" },
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'encryption') or (sys_platform == 'darwin' and extra == 'encryption')", specifier = ">=2023.7.22" },
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'ocsp') or (sys_platform == 'darwin' and extra == 'ocsp')", specifier = ">=2023.7.22" },
{ name = "cryptography", marker = "extra == 'ocsp'", specifier = ">=2.5" },
{ name = "dnspython", specifier = ">=1.16.0,<3.0.0" },
{ name = "dnspython", specifier = ">=2.6.1,<3.0.0" },
{ name = "furo", marker = "extra == 'docs'", specifier = "==2025.7.19" },
{ name = "importlib-metadata", marker = "python_full_version < '3.13' and extra == 'test'", specifier = ">=7.0" },
{ name = "pykerberos", marker = "os_name != 'nt' and extra == 'gssapi'" },
@ -1254,12 +1253,9 @@ coverage = [
{ name = "pytest-cov" },
]
dev = []
gevent = [
{ name = "cffi", marker = "python_full_version == '3.14.*'", specifier = ">=2.0.0b1" },
{ name = "gevent" },
]
gevent = [{ name = "gevent", specifier = ">=20.6.0" }]
mockupdb = [{ name = "mockupdb", git = "https://github.com/mongodb-labs/mongo-mockup-db?rev=master" }]
perf = [{ name = "simplejson" }]
perf = [{ name = "simplejson", specifier = ">=3.17.0" }]
pip = [{ name = "pip" }]
typing = [
{ name = "mypy", specifier = "==1.18.1" },