From 8028eb2026da2a71b125ee46cbbb60e197acf43c Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Mon, 30 Jun 2025 15:47:46 -0700 Subject: [PATCH 1/7] add method to convert case insensitive dict to plain dict --- pymongo/asynchronous/uri_parser.py | 2 +- pymongo/common.py | 3 +++ pymongo/synchronous/uri_parser.py | 2 +- pymongo/uri_parser_shared.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pymongo/asynchronous/uri_parser.py b/pymongo/asynchronous/uri_parser.py index 47c6d72031..ceee45f24f 100644 --- a/pymongo/asynchronous/uri_parser.py +++ b/pymongo/asynchronous/uri_parser.py @@ -184,5 +184,5 @@ async def _parse_srv( return { "nodelist": nodes, - "options": options, + "options": options.as_dict(), } diff --git a/pymongo/common.py b/pymongo/common.py index 96f9f87459..dd40b574ac 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -1083,6 +1083,9 @@ def update(self, other: Mapping[str, Any]) -> None: # type: ignore[override] def cased_key(self, key: str) -> Any: return self.__casedkeys[key.lower()] + def as_dict(self) -> dict[str, Any]: + return {self.__casedkeys[k]: self.__data[k] for k in self} + def has_c() -> bool: """Is the C extension installed?""" diff --git a/pymongo/synchronous/uri_parser.py b/pymongo/synchronous/uri_parser.py index 52b59b8fe8..dffa233eb8 100644 --- a/pymongo/synchronous/uri_parser.py +++ b/pymongo/synchronous/uri_parser.py @@ -184,5 +184,5 @@ def _parse_srv( return { "nodelist": nodes, - "options": options, + "options": options.as_dict(), } diff --git a/pymongo/uri_parser_shared.py b/pymongo/uri_parser_shared.py index 0cef176bf1..3d17a978de 100644 --- a/pymongo/uri_parser_shared.py +++ b/pymongo/uri_parser_shared.py @@ -547,6 +547,6 @@ def _validate_uri( "password": passwd, "database": dbase, "collection": collection, - "options": options, + "options": options.as_dict(), "fqdn": fqdn, } From 3d5ce00cc474201dfe7647654d69838e5b0dbb38 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Mon, 30 Jun 2025 16:02:45 -0700 Subject: [PATCH 2/7] make it a plain dictionary in parse_uri --- pymongo/asynchronous/uri_parser.py | 3 ++- pymongo/synchronous/uri_parser.py | 3 ++- pymongo/uri_parser_shared.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pymongo/asynchronous/uri_parser.py b/pymongo/asynchronous/uri_parser.py index ceee45f24f..b792bbd466 100644 --- a/pymongo/asynchronous/uri_parser.py +++ b/pymongo/asynchronous/uri_parser.py @@ -113,6 +113,7 @@ async def parse_uri( srv_max_hosts, ) ) + result["options"] = result["options"].as_dict() return result @@ -184,5 +185,5 @@ async def _parse_srv( return { "nodelist": nodes, - "options": options.as_dict(), + "options": options, } diff --git a/pymongo/synchronous/uri_parser.py b/pymongo/synchronous/uri_parser.py index dffa233eb8..b3531006a0 100644 --- a/pymongo/synchronous/uri_parser.py +++ b/pymongo/synchronous/uri_parser.py @@ -113,6 +113,7 @@ def parse_uri( srv_max_hosts, ) ) + result["options"] = result["options"].as_dict() return result @@ -184,5 +185,5 @@ def _parse_srv( return { "nodelist": nodes, - "options": options.as_dict(), + "options": options, } diff --git a/pymongo/uri_parser_shared.py b/pymongo/uri_parser_shared.py index 3d17a978de..0cef176bf1 100644 --- a/pymongo/uri_parser_shared.py +++ b/pymongo/uri_parser_shared.py @@ -547,6 +547,6 @@ def _validate_uri( "password": passwd, "database": dbase, "collection": collection, - "options": options.as_dict(), + "options": options, "fqdn": fqdn, } From e26e78a1ab58b7f330c557f4849022f30ae727c4 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Mon, 30 Jun 2025 16:29:24 -0700 Subject: [PATCH 3/7] options is no longer case insensitive, modifying tests to reflect that --- test/asynchronous/test_discovery_and_monitoring.py | 4 ++-- test/test_discovery_and_monitoring.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/asynchronous/test_discovery_and_monitoring.py b/test/asynchronous/test_discovery_and_monitoring.py index 70348c8daf..a41e012f36 100644 --- a/test/asynchronous/test_discovery_and_monitoring.py +++ b/test/asynchronous/test_discovery_and_monitoring.py @@ -90,8 +90,8 @@ async def create_mock_topology(uri, monitor_class=DummyMonitor): replica_set_name = None direct_connection = None load_balanced = None - if "replicaset" in parsed_uri["options"]: - replica_set_name = parsed_uri["options"]["replicaset"] + if "replicaSet" in parsed_uri["options"]: + replica_set_name = parsed_uri["options"]["replicaSet"] if "directConnection" in parsed_uri["options"]: direct_connection = parsed_uri["options"]["directConnection"] if "loadBalanced" in parsed_uri["options"]: diff --git a/test/test_discovery_and_monitoring.py b/test/test_discovery_and_monitoring.py index a0dabaaf8e..f13e940f60 100644 --- a/test/test_discovery_and_monitoring.py +++ b/test/test_discovery_and_monitoring.py @@ -90,8 +90,8 @@ def create_mock_topology(uri, monitor_class=DummyMonitor): replica_set_name = None direct_connection = None load_balanced = None - if "replicaset" in parsed_uri["options"]: - replica_set_name = parsed_uri["options"]["replicaset"] + if "replicaSet" in parsed_uri["options"]: + replica_set_name = parsed_uri["options"]["replicaSet"] if "directConnection" in parsed_uri["options"]: direct_connection = parsed_uri["options"]["directConnection"] if "loadBalanced" in parsed_uri["options"]: From 45de7e64046ea76cb5028eb5bdfd9adc81b10bcd Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Mon, 30 Jun 2025 17:03:01 -0700 Subject: [PATCH 4/7] update tests --- test/test_uri_parser.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index ec1c6c164c..e702c6fdf7 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -142,9 +142,9 @@ def test_split_options(self): self.assertTrue(split_options("wtimeoutms=500")) self.assertEqual({"fsync": True}, split_options("fsync=true")) self.assertEqual({"fsync": False}, split_options("fsync=false")) - self.assertEqual({"authmechanism": "GSSAPI"}, split_options("authMechanism=GSSAPI")) + self.assertEqual({"authMechanism": "GSSAPI"}, split_options("authMechanism=GSSAPI")) self.assertEqual( - {"authmechanism": "SCRAM-SHA-1"}, split_options("authMechanism=SCRAM-SHA-1") + {"authMechanism": "SCRAM-SHA-1"}, split_options("authMechanism=SCRAM-SHA-1") ) self.assertEqual({"authsource": "foobar"}, split_options("authSource=foobar")) self.assertEqual({"maxpoolsize": 50}, split_options("maxpoolsize=50")) @@ -290,12 +290,12 @@ def test_parse_uri(self): self.assertEqual(res, parse_uri('mongodb://localhost/test.name/with "delimiters')) res = copy.deepcopy(orig) - res["options"] = {"readpreference": ReadPreference.SECONDARY.mongos_mode} + res["options"] = {"readPreference": ReadPreference.SECONDARY.mongos_mode} self.assertEqual(res, parse_uri("mongodb://localhost/?readPreference=secondary")) # Various authentication tests res = copy.deepcopy(orig) - res["options"] = {"authmechanism": "SCRAM-SHA-256"} + res["options"] = {"authMechanism": "SCRAM-SHA-256"} res["username"] = "user" res["password"] = "password" self.assertEqual( @@ -303,7 +303,7 @@ def test_parse_uri(self): ) res = copy.deepcopy(orig) - res["options"] = {"authmechanism": "SCRAM-SHA-256", "authsource": "bar"} + res["options"] = {"authMechanism": "SCRAM-SHA-256", "authSource": "bar"} res["username"] = "user" res["password"] = "password" res["database"] = "foo" @@ -315,7 +315,7 @@ def test_parse_uri(self): ) res = copy.deepcopy(orig) - res["options"] = {"authmechanism": "SCRAM-SHA-256"} + res["options"] = {"authMechanism": "SCRAM-SHA-256"} res["username"] = "user" res["password"] = "" self.assertEqual(res, parse_uri("mongodb://user:@localhost/?authMechanism=SCRAM-SHA-256")) @@ -327,7 +327,7 @@ def test_parse_uri(self): self.assertEqual(res, parse_uri("mongodb://user%40domain.com:password@localhost/foo")) res = copy.deepcopy(orig) - res["options"] = {"authmechanism": "GSSAPI"} + res["options"] = {"authMechanism": "GSSAPI"} res["username"] = "user@domain.com" res["password"] = "password" res["database"] = "foo" @@ -337,7 +337,7 @@ def test_parse_uri(self): ) res = copy.deepcopy(orig) - res["options"] = {"authmechanism": "GSSAPI"} + res["options"] = {"authMechanism": "GSSAPI"} res["username"] = "user@domain.com" res["password"] = "" res["database"] = "foo" @@ -457,11 +457,12 @@ def test_tlsinsecure_simple(self): self.maxDiff = None uri = "mongodb://example.com/?tlsInsecure=true" res = { - "tlsAllowInvalidHostnames": True, - "tlsAllowInvalidCertificates": True, + "tlsallowinvalidhostnames": True, + "tlsallowinvalidcertificates": True, "tlsInsecure": True, - "tlsDisableOCSPEndpointCheck": True, + "tlsdisableocspendpointcheck": True, } + print(parse_uri(uri)["options"]) self.assertEqual(res, parse_uri(uri)["options"]) def test_normalize_options(self): @@ -479,8 +480,8 @@ def test_unquote_during_parsing(self): ) res = parse_uri(uri) options: dict[str, Any] = { - "authmechanism": "MONGODB-AWS", - "authmechanismproperties": {"AWS_SESSION_TOKEN": unquoted_val}, + "authMechanism": "MONGODB-AWS", + "authMechanismProperties": {"AWS_SESSION_TOKEN": unquoted_val}, } self.assertEqual(options, res["options"]) @@ -519,7 +520,7 @@ def test_handle_colon(self): ) res = parse_uri(uri) options = { - "authmechanism": "MONGODB-AWS", + "authMechanism": "MONGODB-AWS", "authMechanismProperties": {"AWS_SESSION_TOKEN": token}, } self.assertEqual(options, res["options"]) From fad3686688ca0586d22f35c24000f499323ae967 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Tue, 1 Jul 2025 14:54:56 -0700 Subject: [PATCH 5/7] introduce frozenset of proper cased uri options --- pymongo/asynchronous/uri_parser.py | 3 +- pymongo/common.py | 3 -- pymongo/synchronous/uri_parser.py | 3 +- pymongo/uri_parser_shared.py | 59 ++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/pymongo/asynchronous/uri_parser.py b/pymongo/asynchronous/uri_parser.py index b792bbd466..67456bc2f9 100644 --- a/pymongo/asynchronous/uri_parser.py +++ b/pymongo/asynchronous/uri_parser.py @@ -29,6 +29,7 @@ SCHEME_LEN, SRV_SCHEME_LEN, _check_options, + _make_options_case_sensative, _validate_uri, split_hosts, split_options, @@ -113,7 +114,7 @@ async def parse_uri( srv_max_hosts, ) ) - result["options"] = result["options"].as_dict() + result["options"] = _make_options_case_sensative(result["options"]) return result diff --git a/pymongo/common.py b/pymongo/common.py index dd40b574ac..96f9f87459 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -1083,9 +1083,6 @@ def update(self, other: Mapping[str, Any]) -> None: # type: ignore[override] def cased_key(self, key: str) -> Any: return self.__casedkeys[key.lower()] - def as_dict(self) -> dict[str, Any]: - return {self.__casedkeys[k]: self.__data[k] for k in self} - def has_c() -> bool: """Is the C extension installed?""" diff --git a/pymongo/synchronous/uri_parser.py b/pymongo/synchronous/uri_parser.py index b3531006a0..fade453ba8 100644 --- a/pymongo/synchronous/uri_parser.py +++ b/pymongo/synchronous/uri_parser.py @@ -29,6 +29,7 @@ SCHEME_LEN, SRV_SCHEME_LEN, _check_options, + _make_options_case_sensative, _validate_uri, split_hosts, split_options, @@ -113,7 +114,7 @@ def parse_uri( srv_max_hosts, ) ) - result["options"] = result["options"].as_dict() + result["options"] = _make_options_case_sensative(result["options"]) return result diff --git a/pymongo/uri_parser_shared.py b/pymongo/uri_parser_shared.py index 0cef176bf1..2ae455cd83 100644 --- a/pymongo/uri_parser_shared.py +++ b/pymongo/uri_parser_shared.py @@ -54,6 +54,57 @@ SRV_SCHEME_LEN = len(SRV_SCHEME) DEFAULT_PORT = 27017 +URI_OPTIONS = frozenset( + [ + "appname", + "authMechanism", + "authMechanismProperties", + "authSource", + "compressors", + "connectTimeoutMS", + "directConnection", + "heartbeatFrequencyMS", + "journal", + "loadBalanced", + "localThresholdMS", + "maxIdleTimeMS", + "maxPoolSize", + "maxConnecting", + "maxStalenessSeconds", + "minPoolSize", + "proxyHost", + "proxyPort", + "proxyUsername", + "proxyPassword", + "readConcernLevel", + "readPreference", + "readPreferenceTags", + "replicaSet", + "retryReads", + "retryWrites", + "serverMonitoringMode", + "serverSelectionTimeoutMS", + "serverSelectionTryOnce", + "socketTimeoutMS", + "srvMaxHosts", + "srvServiceName", + "ssl", + "tls", + "tlsAllowInvalidCertificates", + "tlsAllowInvalidHostnames", + "tlsCAFile", + "tlsCertificateKeyFile", + "tlsCertificateKeyFilePassword", + "tlsDisableCertificateRevocationCheck", + "tlsDisableOCSPEndpointCheck", + "tlsInsecure", + "w", + "waitQueueTimeoutMS", + "wTimeoutMS", + "zlibCompressionLevel", + ] +) + def _unquoted_percent(s: str) -> bool: """Check for unescaped percent signs. @@ -550,3 +601,11 @@ def _validate_uri( "options": options, "fqdn": fqdn, } + + +def _make_options_case_sensative(options: _CaseInsensitiveDictionary) -> dict[str, Any]: + case_sensative = {} + for option in URI_OPTIONS: + if option.lower() in options: + case_sensative[option] = options[option] + return case_sensative From 6f705ba35833abebd39ed97d4de7869590fdeaf9 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Tue, 1 Jul 2025 15:56:34 -0700 Subject: [PATCH 6/7] fix typo and tests --- pymongo/asynchronous/uri_parser.py | 4 ++-- pymongo/synchronous/uri_parser.py | 4 ++-- pymongo/uri_parser_shared.py | 11 +++++++---- test/test_uri_parser.py | 18 +++++++++--------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pymongo/asynchronous/uri_parser.py b/pymongo/asynchronous/uri_parser.py index 67456bc2f9..11a6a6299c 100644 --- a/pymongo/asynchronous/uri_parser.py +++ b/pymongo/asynchronous/uri_parser.py @@ -29,7 +29,7 @@ SCHEME_LEN, SRV_SCHEME_LEN, _check_options, - _make_options_case_sensative, + _make_options_case_sensitive, _validate_uri, split_hosts, split_options, @@ -114,7 +114,7 @@ async def parse_uri( srv_max_hosts, ) ) - result["options"] = _make_options_case_sensative(result["options"]) + result["options"] = _make_options_case_sensitive(result["options"]) return result diff --git a/pymongo/synchronous/uri_parser.py b/pymongo/synchronous/uri_parser.py index fade453ba8..da0f86d720 100644 --- a/pymongo/synchronous/uri_parser.py +++ b/pymongo/synchronous/uri_parser.py @@ -29,7 +29,7 @@ SCHEME_LEN, SRV_SCHEME_LEN, _check_options, - _make_options_case_sensative, + _make_options_case_sensitive, _validate_uri, split_hosts, split_options, @@ -114,7 +114,7 @@ def parse_uri( srv_max_hosts, ) ) - result["options"] = _make_options_case_sensative(result["options"]) + result["options"] = _make_options_case_sensitive(result["options"]) return result diff --git a/pymongo/uri_parser_shared.py b/pymongo/uri_parser_shared.py index 2ae455cd83..59168d1e9f 100644 --- a/pymongo/uri_parser_shared.py +++ b/pymongo/uri_parser_shared.py @@ -603,9 +603,12 @@ def _validate_uri( } -def _make_options_case_sensative(options: _CaseInsensitiveDictionary) -> dict[str, Any]: - case_sensative = {} +def _make_options_case_sensitive(options: _CaseInsensitiveDictionary) -> dict[str, Any]: + case_sensitive = {} for option in URI_OPTIONS: if option.lower() in options: - case_sensative[option] = options[option] - return case_sensative + case_sensitive[option] = options[option] + options.pop(option) + for k, v in options.items(): + case_sensitive[k] = v + return case_sensitive diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index e702c6fdf7..ed1a53ea26 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -347,8 +347,8 @@ def test_parse_uri(self): res = copy.deepcopy(orig) res["options"] = { - "readpreference": ReadPreference.SECONDARY.mongos_mode, - "readpreferencetags": [ + "readPreference": ReadPreference.SECONDARY.mongos_mode, + "readPreferenceTags": [ {"dc": "west", "use": "website"}, {"dc": "east", "use": "website"}, ], @@ -368,8 +368,8 @@ def test_parse_uri(self): res = copy.deepcopy(orig) res["options"] = { - "readpreference": ReadPreference.SECONDARY.mongos_mode, - "readpreferencetags": [ + "readPreference": ReadPreference.SECONDARY.mongos_mode, + "readPreferenceTags": [ {"dc": "west", "use": "website"}, {"dc": "east", "use": "website"}, {}, @@ -457,10 +457,10 @@ def test_tlsinsecure_simple(self): self.maxDiff = None uri = "mongodb://example.com/?tlsInsecure=true" res = { - "tlsallowinvalidhostnames": True, - "tlsallowinvalidcertificates": True, + "tlsAllowInvalidHostnames": True, + "tlsAllowInvalidCertificates": True, "tlsInsecure": True, - "tlsdisableocspendpointcheck": True, + "tlsDisableOCSPEndpointCheck": True, } print(parse_uri(uri)["options"]) self.assertEqual(res, parse_uri(uri)["options"]) @@ -492,8 +492,8 @@ def test_unquote_during_parsing(self): ) res = parse_uri(uri) options = { - "readpreference": ReadPreference.SECONDARY.mongos_mode, - "readpreferencetags": [ + "readPreference": ReadPreference.SECONDARY.mongos_mode, + "readPreferenceTags": [ {"dc": "west", unquoted_val: unquoted_val}, {"dc": "east", "use": unquoted_val}, ], From 988afaed46d718d7d6da74dc3973d2a6d875b004 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Tue, 1 Jul 2025 16:38:31 -0700 Subject: [PATCH 7/7] test can use case insensitive dict --- test/test_uri_spec.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_uri_spec.py b/test/test_uri_spec.py index aeb0be94b5..8f673cff4c 100644 --- a/test/test_uri_spec.py +++ b/test/test_uri_spec.py @@ -27,7 +27,7 @@ from test import unittest from test.helpers import clear_warning_registry -from pymongo.common import INTERNAL_URI_OPTION_NAME_MAP, validate +from pymongo.common import INTERNAL_URI_OPTION_NAME_MAP, _CaseInsensitiveDictionary, validate from pymongo.compression_support import _have_snappy from pymongo.synchronous.uri_parser import parse_uri @@ -169,7 +169,8 @@ def run_scenario(self): # Compare URI options. err_msg = "For option %s expected %s but got %s" if test["options"]: - opts = options["options"] + opts = _CaseInsensitiveDictionary() + opts.update(options["options"]) for opt in test["options"]: lopt = opt.lower() optname = INTERNAL_URI_OPTION_NAME_MAP.get(lopt, lopt)