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

This commit is contained in:
Steven Silvester 2025-04-10 09:34:05 -05:00
commit ecd548fc04
No known key found for this signature in database
GPG Key ID: B1BF5EC3A8B32F91
12 changed files with 442 additions and 170 deletions

View File

@ -820,19 +820,7 @@ tasks:
- name: coverage-report
commands:
- func: download and merge coverage
depends_on:
- name: .standalone
variant: .coverage_tag
status: "*"
patch_optional: true
- name: .replica_set
variant: .coverage_tag
status: "*"
patch_optional: true
- name: .sharded_cluster
variant: .coverage_tag
status: "*"
patch_optional: true
depends_on: [{ name: .server-version, variant: .coverage_tag, status: "*", patch_optional: true }]
tags: [coverage]
# Doctest tests
@ -8031,6 +8019,274 @@ tasks:
- nossl
- sync_async
# Server version tests
- name: test-python3.9-auth-ssl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.9"
tags: [server-version, "3.9", sharded_cluster-auth-ssl]
- name: test-python3.10-auth-ssl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.10"
tags: [server-version, "3.10", sharded_cluster-auth-ssl]
- name: test-python3.11-auth-ssl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.11"
tags: [server-version, "3.11", sharded_cluster-auth-ssl]
- name: test-python3.12-auth-ssl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.12"
tags: [server-version, "3.12", sharded_cluster-auth-ssl]
- name: test-python3.13-auth-ssl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.13"
tags: [server-version, "3.13", sharded_cluster-auth-ssl]
- name: test-pypy3.10-auth-ssl-sharded-cluster
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: sharded_cluster
PYTHON_VERSION: pypy3.10
tags: [server-version, pypy3.10, sharded_cluster-auth-ssl]
- name: test-python3.9-auth-ssl-standalone-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: standalone
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: standalone
COVERAGE: "1"
PYTHON_VERSION: "3.9"
tags: [server-version, "3.9", standalone-auth-ssl]
- name: test-python3.10-auth-nossl-standalone-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: nossl
TOPOLOGY: standalone
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: nossl
TOPOLOGY: standalone
COVERAGE: "1"
PYTHON_VERSION: "3.10"
tags: [server-version, "3.10", standalone-auth-nossl]
- name: test-python3.11-noauth-ssl-standalone-cov
commands:
- func: run server
vars:
AUTH: noauth
SSL: ssl
TOPOLOGY: standalone
COVERAGE: "1"
- func: run tests
vars:
AUTH: noauth
SSL: ssl
TOPOLOGY: standalone
COVERAGE: "1"
PYTHON_VERSION: "3.11"
tags: [server-version, "3.11", standalone-noauth-ssl]
- name: test-python3.12-noauth-nossl-standalone-cov
commands:
- func: run server
vars:
AUTH: noauth
SSL: nossl
TOPOLOGY: standalone
COVERAGE: "1"
- func: run tests
vars:
AUTH: noauth
SSL: nossl
TOPOLOGY: standalone
COVERAGE: "1"
PYTHON_VERSION: "3.12"
tags: [server-version, "3.12", standalone-noauth-nossl]
- name: test-python3.13-auth-ssl-replica-set-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: replica_set
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: ssl
TOPOLOGY: replica_set
COVERAGE: "1"
PYTHON_VERSION: "3.13"
tags: [server-version, "3.13", replica_set-auth-ssl]
- name: test-pypy3.10-auth-nossl-replica-set
commands:
- func: run server
vars:
AUTH: auth
SSL: nossl
TOPOLOGY: replica_set
- func: run tests
vars:
AUTH: auth
SSL: nossl
TOPOLOGY: replica_set
PYTHON_VERSION: pypy3.10
tags: [server-version, pypy3.10, replica_set-auth-nossl]
- name: test-python3.9-noauth-ssl-replica-set-cov
commands:
- func: run server
vars:
AUTH: noauth
SSL: ssl
TOPOLOGY: replica_set
COVERAGE: "1"
- func: run tests
vars:
AUTH: noauth
SSL: ssl
TOPOLOGY: replica_set
COVERAGE: "1"
PYTHON_VERSION: "3.9"
tags: [server-version, "3.9", replica_set-noauth-ssl]
- name: test-python3.10-noauth-nossl-replica-set-cov
commands:
- func: run server
vars:
AUTH: noauth
SSL: nossl
TOPOLOGY: replica_set
COVERAGE: "1"
- func: run tests
vars:
AUTH: noauth
SSL: nossl
TOPOLOGY: replica_set
COVERAGE: "1"
PYTHON_VERSION: "3.10"
tags: [server-version, "3.10", replica_set-noauth-nossl]
- name: test-python3.12-auth-nossl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: auth
SSL: nossl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: auth
SSL: nossl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.12"
tags: [server-version, "3.12", sharded_cluster-auth-nossl]
- name: test-python3.13-noauth-ssl-sharded-cluster-cov
commands:
- func: run server
vars:
AUTH: noauth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
- func: run tests
vars:
AUTH: noauth
SSL: ssl
TOPOLOGY: sharded_cluster
COVERAGE: "1"
PYTHON_VERSION: "3.13"
tags: [server-version, "3.13", sharded_cluster-noauth-ssl]
- name: test-pypy3.10-noauth-nossl-sharded-cluster
commands:
- func: run server
vars:
AUTH: noauth
SSL: nossl
TOPOLOGY: sharded_cluster
- func: run tests
vars:
AUTH: noauth
SSL: nossl
TOPOLOGY: sharded_cluster
PYTHON_VERSION: pypy3.10
tags: [server-version, pypy3.10, sharded_cluster-noauth-nossl]
# Serverless tests
- name: test-serverless
commands:

View File

@ -805,114 +805,6 @@ buildvariants:
PYTHON_BINARY: /opt/python/3.9/bin/python3
# Server tests
- name: test-rhel8-python3.9-cov-no-c
tasks:
- name: .standalone .sync_async
- name: .replica_set .sync_async
- name: .sharded_cluster .sync_async
display_name: "* Test RHEL8 Python3.9 cov No C"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
NO_EXT: "1"
PYTHON_BINARY: /opt/python/3.9/bin/python3
tags: [coverage_tag]
- name: test-rhel8-python3.9-cov
tasks:
- name: .standalone .sync_async
- name: .replica_set .sync_async
- name: .sharded_cluster .sync_async
display_name: "* Test RHEL8 Python3.9 cov"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
PYTHON_BINARY: /opt/python/3.9/bin/python3
tags: [coverage_tag]
- name: test-rhel8-python3.13-cov-no-c
tasks:
- name: .standalone .sync_async
- name: .replica_set .sync_async
- name: .sharded_cluster .sync_async
display_name: "* Test RHEL8 Python3.13 cov No C"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
NO_EXT: "1"
PYTHON_BINARY: /opt/python/3.13/bin/python3
tags: [coverage_tag]
- name: test-rhel8-python3.13-cov
tasks:
- name: .standalone .sync_async
- name: .replica_set .sync_async
- name: .sharded_cluster .sync_async
display_name: "* Test RHEL8 Python3.13 cov"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
PYTHON_BINARY: /opt/python/3.13/bin/python3
tags: [coverage_tag]
- name: test-rhel8-pypy3.10-cov-no-c
tasks:
- name: .standalone .sync_async
- name: .replica_set .sync_async
- name: .sharded_cluster .sync_async
display_name: "* Test RHEL8 PyPy3.10 cov No C"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
NO_EXT: "1"
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3
tags: [coverage_tag]
- name: test-rhel8-pypy3.10-cov
tasks:
- name: .standalone .sync_async
- name: .replica_set .sync_async
- name: .sharded_cluster .sync_async
display_name: "* Test RHEL8 PyPy3.10 cov"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3
tags: [coverage_tag]
- name: test-rhel8-python3.10
tasks:
- name: .sharded_cluster .auth .ssl .sync_async
- name: .replica_set .noauth .ssl .sync_async
- name: .standalone .noauth .nossl .sync_async
display_name: "* Test RHEL8 Python3.10"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
PYTHON_BINARY: /opt/python/3.10/bin/python3
- name: test-rhel8-python3.11
tasks:
- name: .sharded_cluster .auth .ssl .sync_async
- name: .replica_set .noauth .ssl .sync_async
- name: .standalone .noauth .nossl .sync_async
display_name: "* Test RHEL8 Python3.11"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
PYTHON_BINARY: /opt/python/3.11/bin/python3
- name: test-rhel8-python3.12
tasks:
- name: .sharded_cluster .auth .ssl .sync_async
- name: .replica_set .noauth .ssl .sync_async
- name: .standalone .noauth .nossl .sync_async
display_name: "* Test RHEL8 Python3.12"
run_on:
- rhel87-small
expansions:
COVERAGE: coverage
PYTHON_BINARY: /opt/python/3.12/bin/python3
- name: test-macos-python3.9
tasks:
- name: .sharded_cluster .auth .ssl !.sync_async
@ -1018,6 +910,71 @@ buildvariants:
expansions:
PYTHON_BINARY: C:/python/32/Python313/python.exe
# Server version tests
- name: mongodb-v4.0
tasks:
- name: .server-version
display_name: "* MongoDB v4.0"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-v4.2
tasks:
- name: .server-version
display_name: "* MongoDB v4.2"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-v4.4
tasks:
- name: .server-version
display_name: "* MongoDB v4.4"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-v5.0
tasks:
- name: .server-version
display_name: "* MongoDB v5.0"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-v6.0
tasks:
- name: .server-version
display_name: "* MongoDB v6.0"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-v7.0
tasks:
- name: .server-version
display_name: "* MongoDB v7.0"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-v8.0
tasks:
- name: .server-version
display_name: "* MongoDB v8.0"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-rapid
tasks:
- name: .server-version
display_name: "* MongoDB rapid"
run_on:
- rhel87-small
tags: [coverage_tag]
- name: mongodb-latest
tasks:
- name: .server-version
display_name: "* MongoDB latest"
run_on:
- rhel87-small
tags: [coverage_tag]
# Serverless tests
- name: serverless-rhel8-python3.9
tasks:

View File

@ -66,39 +66,20 @@ def create_ocsp_variants() -> list[BuildVariant]:
return variants
def create_server_version_variants() -> list[BuildVariant]:
variants = []
for version in ALL_VERSIONS:
display_name = get_variant_name("* MongoDB", version=version)
variant = create_variant(
[".server-version"], display_name, host=DEFAULT_HOST, tags=["coverage_tag"]
)
variants.append(variant)
return variants
def create_server_variants() -> list[BuildVariant]:
variants = []
# Run the full matrix on linux with min and max CPython, and latest pypy.
host = DEFAULT_HOST
# Prefix the display name with an asterisk so it is sorted first.
base_display_name = "* Test"
for python, c_ext in product([*MIN_MAX_PYTHON, PYPYS[-1]], C_EXTS):
expansions = dict(COVERAGE="coverage")
handle_c_ext(c_ext, expansions)
display_name = get_variant_name(base_display_name, host, python=python, **expansions)
variant = create_variant(
[f".{t} .sync_async" for t in TOPOLOGIES],
display_name,
python=python,
host=host,
tags=["coverage_tag"],
expansions=expansions,
)
variants.append(variant)
# Test the rest of the pythons.
for python in CPYTHONS[1:-1] + PYPYS[:-1]:
display_name = f"Test {host}"
display_name = get_variant_name(base_display_name, host, python=python)
variant = create_variant(
[f"{t} .sync_async" for t in SUB_TASKS],
display_name,
python=python,
host=host,
expansions=expansions,
)
variants.append(variant)
# Test a subset on each of the other platforms.
for host_name in ("macos", "macos-arm64", "win64", "win32"):
@ -597,6 +578,32 @@ def create_aws_lambda_variants():
##############
def create_server_version_tasks():
tasks = []
# Test all pythons with sharded_cluster, auth, and ssl.
task_types = [(p, "sharded_cluster", "auth", "ssl") for p in ALL_PYTHONS]
# Test all combinations of topology, auth, and ssl, with rotating pythons.
for (topology, auth, ssl), python in zip_cycle(
list(product(TOPOLOGIES, ["auth", "noauth"], ["ssl", "nossl"])), ALL_PYTHONS
):
# Skip the ones we already have.
if topology == "sharded_cluster" and auth == "auth" and ssl == "ssl":
continue
task_types.append((python, topology, auth, ssl))
for python, topology, auth, ssl in task_types:
tags = ["server-version", python, f"{topology}-{auth}-{ssl}"]
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
if python not in PYPYS:
expansions["COVERAGE"] = "1"
name = get_task_name("test", python=python, **expansions)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
test_vars["PYTHON_VERSION"] = python
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_server_tasks():
tasks = []
for topo, version, (auth, ssl), sync in product(TOPOLOGIES, ALL_VERSIONS, AUTH_SSLS, SYNCS):
@ -882,11 +889,11 @@ def create_coverage_report_tasks():
# Instead list out all coverage tasks using tags.
# Run the coverage task even if some tasks fail.
# Run the coverage task even if some tasks are not scheduled in a patch build.
task_deps = []
for name in [".standalone", ".replica_set", ".sharded_cluster"]:
task_deps.append(
EvgTaskDependency(name=name, variant=".coverage_tag", status="*", patch_optional=True)
task_deps = [
EvgTaskDependency(
name=".server-version", variant=".coverage_tag", status="*", patch_optional=True
)
]
cmd = FunctionCall(func="download and merge coverage")
return [EvgTask(name=task_name, tags=tags, depends_on=task_deps, commands=[cmd])]

View File

@ -40,8 +40,11 @@ SYNCS = ["sync", "async", "sync_async"]
DISPLAY_LOOKUP = dict(
ssl=dict(ssl="SSL", nossl="NoSSL"),
auth=dict(auth="Auth", noauth="NoAuth"),
topology=dict(
standalone="Standalone", replica_set="Replica Set", sharded_cluster="Sharded Cluster"
),
test_suites=dict(default="Sync", default_async="Async"),
coverage=dict(coverage="cov"),
coverage={"1": "cov"},
no_ext={"1": "No C"},
)
HOSTS = dict()

View File

@ -1,6 +1,23 @@
Changelog
=========
Changes in Version 4.12.1 (XXXX/XX/XX)
--------------------------------------
Version 4.12.1 is a bug fix release.
- Fixed a bug that could raise ``UnboundLocalError`` when creating asynchronous connections over SSL.
- Fixed a bug causing SRV hostname validation to fail when resolver and resolved hostnames are identical with three domain levels.
Issues Resolved
...............
See the `PyMongo 4.12.1 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.12.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43094
Changes in Version 4.12.0 (2025/04/08)
--------------------------------------

View File

@ -96,6 +96,7 @@ class _SrvResolver:
except Exception:
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None
self.__slen = len(self.__plist)
self.nparts = len(split_fqdn)
async def get_options(self) -> Optional[str]:
from dns import resolver
@ -137,12 +138,13 @@ class _SrvResolver:
# Validate hosts
for node in nodes:
if self.__fqdn == node[0].lower():
srv_host = node[0].lower()
if self.__fqdn == srv_host and self.nparts < 3:
raise ConfigurationError(
"Invalid SRV host: return address is identical to SRV hostname"
)
try:
nlist = node[0].lower().split(".")[1:][-self.__slen :]
nlist = srv_host.split(".")[1:][-self.__slen :]
except Exception:
raise ConfigurationError(f"Invalid SRV host: {node[0]}") from None
if self.__plist != nlist:

View File

@ -346,12 +346,10 @@ async def _configured_protocol_interface(
ssl=ssl_context,
)
except _CertificateError:
transport.abort()
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
transport.abort()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
# mismatch, will be turned into ServerSelectionTimeoutErrors later.

View File

@ -96,6 +96,7 @@ class _SrvResolver:
except Exception:
raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None
self.__slen = len(self.__plist)
self.nparts = len(split_fqdn)
def get_options(self) -> Optional[str]:
from dns import resolver
@ -137,12 +138,13 @@ class _SrvResolver:
# Validate hosts
for node in nodes:
if self.__fqdn == node[0].lower():
srv_host = node[0].lower()
if self.__fqdn == srv_host and self.nparts < 3:
raise ConfigurationError(
"Invalid SRV host: return address is identical to SRV hostname"
)
try:
nlist = node[0].lower().split(".")[1:][-self.__slen :]
nlist = srv_host.split(".")[1:][-self.__slen :]
except Exception:
raise ConfigurationError(f"Invalid SRV host: {node[0]}") from None
if self.__plist != nlist:

View File

@ -220,12 +220,15 @@ class TestInitialDnsSeedlistDiscovery(AsyncPyMongoTestCase):
mock_resolver.side_effect = mock_resolve
domain = case["query"].split("._tcp.")[1]
connection_string = f"mongodb+srv://{domain}"
try:
if "expected_error" not in case:
await parse_uri(connection_string)
except ConfigurationError as e:
self.assertIn(case["expected_error"], str(e))
else:
self.fail(f"ConfigurationError was not raised for query: {case['query']}")
try:
await parse_uri(connection_string)
except ConfigurationError as e:
self.assertIn(case["expected_error"], str(e))
else:
self.fail(f"ConfigurationError was not raised for query: {case['query']}")
async def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self):
with patch("dns.asyncresolver.resolve"):
@ -289,6 +292,17 @@ class TestInitialDnsSeedlistDiscovery(AsyncPyMongoTestCase):
]
await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases)
async def test_5_when_srv_hostname_has_two_dot_separated_parts_it_is_valid_for_the_returned_hostname_to_be_identical(
self
):
test_cases = [
{
"query": "_mongodb._tcp.blogs.mongodb.com",
"mock_target": "blogs.mongodb.com",
},
]
await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases)
if __name__ == "__main__":
unittest.main()

View File

@ -57,6 +57,7 @@ class TestMonitor(AsyncIntegrationTest):
await connected(client)
return client
@unittest.skipIf("PyPy" in sys.version, "PYTHON-5283 fails often on PyPy")
async def test_cleanup_executors_on_client_del(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")

View File

@ -218,12 +218,15 @@ class TestInitialDnsSeedlistDiscovery(PyMongoTestCase):
mock_resolver.side_effect = mock_resolve
domain = case["query"].split("._tcp.")[1]
connection_string = f"mongodb+srv://{domain}"
try:
if "expected_error" not in case:
parse_uri(connection_string)
except ConfigurationError as e:
self.assertIn(case["expected_error"], str(e))
else:
self.fail(f"ConfigurationError was not raised for query: {case['query']}")
try:
parse_uri(connection_string)
except ConfigurationError as e:
self.assertIn(case["expected_error"], str(e))
else:
self.fail(f"ConfigurationError was not raised for query: {case['query']}")
def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self):
with patch("dns.resolver.resolve"):
@ -287,6 +290,17 @@ class TestInitialDnsSeedlistDiscovery(PyMongoTestCase):
]
self.run_initial_dns_seedlist_discovery_prose_tests(test_cases)
def test_5_when_srv_hostname_has_two_dot_separated_parts_it_is_valid_for_the_returned_hostname_to_be_identical(
self
):
test_cases = [
{
"query": "_mongodb._tcp.blogs.mongodb.com",
"mock_target": "blogs.mongodb.com",
},
]
self.run_initial_dns_seedlist_discovery_prose_tests(test_cases)
if __name__ == "__main__":
unittest.main()

View File

@ -57,6 +57,7 @@ class TestMonitor(IntegrationTest):
connected(client)
return client
@unittest.skipIf("PyPy" in sys.version, "PYTHON-5283 fails often on PyPy")
def test_cleanup_executors_on_client_del(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")