Skip to content

PYTHON-5421 Make parse_uri() return "options" as a dict rather than _CaseInsensitiveDictionary #2413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pymongo/asynchronous/uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
SCHEME_LEN,
SRV_SCHEME_LEN,
_check_options,
_make_options_case_sensitive,
_validate_uri,
split_hosts,
split_options,
Expand Down Expand Up @@ -113,6 +114,7 @@ async def parse_uri(
srv_max_hosts,
)
)
result["options"] = _make_options_case_sensitive(result["options"])
return result


Expand Down
2 changes: 2 additions & 0 deletions pymongo/synchronous/uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
SCHEME_LEN,
SRV_SCHEME_LEN,
_check_options,
_make_options_case_sensitive,
_validate_uri,
split_hosts,
split_options,
Expand Down Expand Up @@ -113,6 +114,7 @@ def parse_uri(
srv_max_hosts,
)
)
result["options"] = _make_options_case_sensitive(result["options"])
return result


Expand Down
62 changes: 62 additions & 0 deletions pymongo/uri_parser_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -550,3 +601,14 @@ def _validate_uri(
"options": options,
"fqdn": fqdn,
}


def _make_options_case_sensitive(options: _CaseInsensitiveDictionary) -> dict[str, Any]:
case_sensitive = {}
for option in URI_OPTIONS:
if option.lower() in options:
case_sensitive[option] = options[option]
options.pop(option)
for k, v in options.items():
case_sensitive[k] = v
return case_sensitive
4 changes: 2 additions & 2 deletions test/asynchronous/test_discovery_and_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down
4 changes: 2 additions & 2 deletions test/test_discovery_and_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down
35 changes: 18 additions & 17 deletions test/test_uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -290,20 +290,20 @@ 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(
res, parse_uri("mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256")
)

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"
Expand All @@ -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"))
Expand All @@ -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"] = "[email protected]"
res["password"] = "password"
res["database"] = "foo"
Expand All @@ -337,7 +337,7 @@ def test_parse_uri(self):
)

res = copy.deepcopy(orig)
res["options"] = {"authmechanism": "GSSAPI"}
res["options"] = {"authMechanism": "GSSAPI"}
res["username"] = "[email protected]"
res["password"] = ""
res["database"] = "foo"
Expand All @@ -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"},
],
Expand All @@ -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"},
{},
Expand Down Expand Up @@ -462,6 +462,7 @@ def test_tlsinsecure_simple(self):
"tlsInsecure": True,
"tlsDisableOCSPEndpointCheck": True,
}
print(parse_uri(uri)["options"])
self.assertEqual(res, parse_uri(uri)["options"])

def test_normalize_options(self):
Expand All @@ -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"])

Expand All @@ -491,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},
],
Expand All @@ -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"])
Expand Down
5 changes: 3 additions & 2 deletions test/test_uri_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
Loading