Skip to content

PYTHON-3636 MongoClient should perform SRV resolution lazily #2191

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 64 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
ead780a
WIP (not cleaned up)
sleepyStick Mar 7, 2025
79c09ea
this might be broken? unsure....
sleepyStick Mar 10, 2025
3afd732
Merge branch 'mongodb:master' into PYTHON-3636
sleepyStick Mar 10, 2025
7d771cb
keep parse_uri as is and have it call two different functions instead
sleepyStick Mar 10, 2025
0f64689
cleanup
sleepyStick Mar 10, 2025
ed50141
some refactoring to reduce code duplication
sleepyStick Mar 11, 2025
ed25867
fix typing
sleepyStick Mar 11, 2025
8d48f44
remove copied doc string
sleepyStick Mar 11, 2025
1a3efed
move init_background to only be called upon client connection
sleepyStick Mar 11, 2025
d94743b
only define topology after uri resolution
sleepyStick Mar 11, 2025
ad20606
okay turns out it was *too* lazy HAHA
sleepyStick Mar 11, 2025
dfa0639
cleanup
sleepyStick Mar 11, 2025
57edcbc
more cleanup
sleepyStick Mar 12, 2025
58a58a0
more cleanup
sleepyStick Mar 12, 2025
35a41e9
fix fork tests
sleepyStick Mar 12, 2025
d343311
fix typing
sleepyStick Mar 12, 2025
d03c78f
determine is_srv differently
sleepyStick Mar 12, 2025
e1d091f
fix test
sleepyStick Mar 12, 2025
8efd549
fix encrypter
sleepyStick Mar 12, 2025
4c06dec
undoing unintended changes
sleepyStick Mar 12, 2025
511fcc4
bringing back a previously deleted test
sleepyStick Mar 12, 2025
40509a1
undoing unintended changes
sleepyStick Mar 12, 2025
97e0778
some refactoring
sleepyStick Mar 12, 2025
1de56d4
Merge branch 'master' into PYTHON-3636
sleepyStick Mar 12, 2025
2653a56
fix typing
sleepyStick Mar 12, 2025
4c23ee0
Update pymongo/asynchronous/mongo_client.py
sleepyStick Mar 13, 2025
bc61199
Update pymongo/asynchronous/mongo_client.py
sleepyStick Mar 13, 2025
8c2b368
respond to comments and move srv_resolver to async
sleepyStick Mar 13, 2025
e38c2ad
refactor part 2
sleepyStick Mar 13, 2025
3c1bb28
Merge branch 'master' into PYTHON-3636
sleepyStick Mar 13, 2025
94fec44
fix circular import
sleepyStick Mar 13, 2025
99a07fe
Merge branch 'PYTHON-3636' of github.com:sleepyStick/mongo-python-dri…
sleepyStick Mar 13, 2025
32fabb9
fix tests
sleepyStick Mar 13, 2025
af568da
fix test and repr
sleepyStick Mar 13, 2025
82bcd38
fix test
sleepyStick Mar 13, 2025
60bf17d
fix import for test
sleepyStick Mar 13, 2025
63ba7be
change helpers import
sleepyStick Mar 13, 2025
7585e04
fix uri_parser
sleepyStick Mar 13, 2025
2c69412
fix srv_resolver
sleepyStick Mar 13, 2025
f834b89
add missing awaits
sleepyStick Mar 13, 2025
d450457
add missing await
sleepyStick Mar 13, 2025
2a8b1b2
Update test/auth_aws/test_auth_aws.py
sleepyStick Mar 14, 2025
c82cf50
Update test/asynchronous/helpers.py
sleepyStick Mar 14, 2025
9256808
Update test/auth_oidc/test_auth_oidc.py
sleepyStick Mar 14, 2025
c6d2ceb
address comments
sleepyStick Mar 14, 2025
d8d2c26
Merge branch 'PYTHON-3636' of github.com:sleepyStick/mongo-python-dri…
sleepyStick Mar 14, 2025
8927a27
undo import change in helpers
sleepyStick Mar 14, 2025
76a68b2
change client eq and hash
sleepyStick Mar 17, 2025
b60eb60
address comments part 1
sleepyStick Mar 17, 2025
0b6d303
address comment ish - remove first
sleepyStick Mar 18, 2025
0ca6afd
re-order call to super's init
sleepyStick Mar 18, 2025
259d36b
Merge branch 'main' into PYTHON-3636
sleepyStick Mar 18, 2025
d616135
update link to use https based on prev commit on main
sleepyStick Mar 18, 2025
379dfb6
fix typing
sleepyStick Mar 18, 2025
5466484
oops fix typing pt2
sleepyStick Mar 18, 2025
2900718
address comments
sleepyStick Mar 21, 2025
63676b6
fix patch string
sleepyStick Mar 21, 2025
cd9bd92
address comments pt1
sleepyStick Mar 21, 2025
f33d091
Merge branch 'main' into PYTHON-3636
sleepyStick Mar 21, 2025
a7c090d
add test for repr and change changelog
sleepyStick Mar 21, 2025
93bc3c9
fix test
sleepyStick Mar 21, 2025
afd82f4
Update doc/changelog.rst
ShaneHarvey Mar 24, 2025
99a5c8a
Address review
NoahStapp Mar 24, 2025
21d3f58
Merge branch 'master' into PYTHON-3636
ShaneHarvey Mar 25, 2025
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
271 changes: 191 additions & 80 deletions pymongo/asynchronous/mongo_client.py

Large diffs are not rendered by default.

271 changes: 191 additions & 80 deletions pymongo/synchronous/mongo_client.py

Large diffs are not rendered by default.

102 changes: 87 additions & 15 deletions pymongo/uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,30 @@ def parse_uri(
.. versionchanged:: 3.1
``warn`` added so invalid options can be ignored.
"""
result = _validate_uri(uri, default_port, validate, warn, normalize, srv_max_hosts)
result.update(
_parse_srv(
uri,
default_port,
validate,
warn,
normalize,
connect_timeout,
srv_service_name,
srv_max_hosts,
)
)
return result


def _validate_uri(
uri: str,
default_port: Optional[int] = DEFAULT_PORT,
validate: bool = True,
warn: bool = False,
normalize: bool = True,
srv_max_hosts: Optional[int] = None,
) -> dict[str, Any]:
if uri.startswith(SCHEME):
is_srv = False
scheme_free = uri[SCHEME_LEN:]
Expand Down Expand Up @@ -527,8 +551,6 @@ def parse_uri(

if opts:
options.update(split_options(opts, validate, warn, normalize))
if srv_service_name is None:
srv_service_name = options.get("srvServiceName", SRV_SERVICE_NAME)
if "@" in host_part:
userinfo, _, hosts = host_part.rpartition("@")
user, passwd = parse_userinfo(userinfo)
Expand All @@ -550,6 +572,69 @@ def parse_uri(
fqdn, port = nodes[0]
if port is not None:
raise InvalidURI(f"{SRV_SCHEME} URIs must not include a port number")
elif not is_srv and options.get("srvServiceName") is not None:
raise ConfigurationError(
"The srvServiceName option is only allowed with 'mongodb+srv://' URIs"
)
elif not is_srv and srv_max_hosts:
raise ConfigurationError(
"The srvMaxHosts option is only allowed with 'mongodb+srv://' URIs"
)
else:
nodes = split_hosts(hosts, default_port=default_port)

_check_options(nodes, options)

return {
"nodelist": nodes,
"username": user,
"password": passwd,
"database": dbase,
"collection": collection,
"options": options,
"fqdn": fqdn,
}


def _parse_srv(
uri: str,
default_port: Optional[int] = DEFAULT_PORT,
validate: bool = True,
warn: bool = False,
normalize: bool = True,
connect_timeout: Optional[float] = None,
srv_service_name: Optional[str] = None,
srv_max_hosts: Optional[int] = None,
) -> dict[str, Any]:
if uri.startswith(SCHEME):
is_srv = False
scheme_free = uri[SCHEME_LEN:]
else:
is_srv = True
scheme_free = uri[SRV_SCHEME_LEN:]

options = _CaseInsensitiveDictionary()

host_plus_db_part, _, opts = scheme_free.partition("?")
if "/" in host_plus_db_part:
host_part, _, _ = host_plus_db_part.partition("/")
else:
host_part = host_plus_db_part

if opts:
options.update(split_options(opts, validate, warn, normalize))
if srv_service_name is None:
srv_service_name = options.get("srvServiceName", SRV_SERVICE_NAME)
if "@" in host_part:
_, _, hosts = host_part.rpartition("@")
else:
hosts = host_part

hosts = unquote_plus(hosts)
srv_max_hosts = srv_max_hosts or options.get("srvMaxHosts")
if is_srv:
nodes = split_hosts(hosts, default_port=None)
fqdn, port = nodes[0]

# Use the connection timeout. connectTimeoutMS passed as a keyword
# argument overrides the same option passed in the connection string.
Expand All @@ -572,27 +657,14 @@ def parse_uri(
raise InvalidURI("You cannot specify replicaSet with srvMaxHosts")
if "tls" not in options and "ssl" not in options:
options["tls"] = True if validate else "true"
elif not is_srv and options.get("srvServiceName") is not None:
raise ConfigurationError(
"The srvServiceName option is only allowed with 'mongodb+srv://' URIs"
)
elif not is_srv and srv_max_hosts:
raise ConfigurationError(
"The srvMaxHosts option is only allowed with 'mongodb+srv://' URIs"
)
else:
nodes = split_hosts(hosts, default_port=default_port)

_check_options(nodes, options)

return {
"nodelist": nodes,
"username": user,
"password": passwd,
"database": dbase,
"collection": collection,
"options": options,
"fqdn": fqdn,
}


Expand Down
9 changes: 9 additions & 0 deletions test/asynchronous/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1911,28 +1911,37 @@ async def test_service_name_from_kwargs(self):
srvServiceName="customname",
connect=False,
)
await client.aconnect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these open/close calls still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes because these are srv uris

self.assertEqual(client._topology_settings.srv_service_name, "customname")
await client.close()
client = AsyncMongoClient(
"mongodb+srv://user:[email protected]"
"/?srvServiceName=shouldbeoverriden",
srvServiceName="customname",
connect=False,
)
await client.aconnect()
self.assertEqual(client._topology_settings.srv_service_name, "customname")
await client.close()
client = AsyncMongoClient(
"mongodb+srv://user:[email protected]/?srvServiceName=customname",
connect=False,
)
await client.aconnect()
self.assertEqual(client._topology_settings.srv_service_name, "customname")
await client.close()

async def test_srv_max_hosts_kwarg(self):
client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/")
await client.aconnect()
self.assertGreater(len(client.topology_description.server_descriptions()), 1)
client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1)
await client.aconnect()
self.assertEqual(len(client.topology_description.server_descriptions()), 1)
client = self.simple_client(
"mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1", srvmaxhosts=2
)
await client.aconnect()
self.assertEqual(len(client.topology_description.server_descriptions()), 2)

@unittest.skipIf(
Expand Down
37 changes: 13 additions & 24 deletions test/asynchronous/test_dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,35 +185,24 @@ def create_tests(cls):

class TestParsingErrors(AsyncPyMongoTestCase):
async def test_invalid_host(self):
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: mongodb is not",
self.simple_client,
"mongodb+srv://mongodb",
)
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: mongodb.com is not",
self.simple_client,
"mongodb+srv://mongodb.com",
)
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: an IP address is not",
self.simple_client,
"mongodb+srv://127.0.0.1",
)
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: an IP address is not",
self.simple_client,
"mongodb+srv://[::1]",
)
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb is not"):
client = self.simple_client("mongodb+srv://mongodb")
await client.aconnect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior change is important to call out in the changelog and jira ticket.

with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb.com is not"):
client = self.simple_client("mongodb+srv://mongodb.com")
await client.aconnect()
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: an IP address is not"):
client = self.simple_client("mongodb+srv://127.0.0.1")
await client.aconnect()
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: an IP address is not"):
client = self.simple_client("mongodb+srv://[::1]")
await client.aconnect()


class IsolatedAsyncioTestCaseInsensitive(AsyncIntegrationTest):
async def test_connect_case_insensitive(self):
client = self.simple_client("mongodb+srv://TEST1.TEST.BUILD.10GEN.cc/")
await client.aconnect()
self.assertGreater(len(client.topology_description.server_descriptions()), 1)


Expand Down
9 changes: 9 additions & 0 deletions test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1868,28 +1868,37 @@ def test_service_name_from_kwargs(self):
srvServiceName="customname",
connect=False,
)
client._connect()
self.assertEqual(client._topology_settings.srv_service_name, "customname")
client.close()
client = MongoClient(
"mongodb+srv://user:[email protected]"
"/?srvServiceName=shouldbeoverriden",
srvServiceName="customname",
connect=False,
)
client._connect()
self.assertEqual(client._topology_settings.srv_service_name, "customname")
client.close()
client = MongoClient(
"mongodb+srv://user:[email protected]/?srvServiceName=customname",
connect=False,
)
client._connect()
self.assertEqual(client._topology_settings.srv_service_name, "customname")
client.close()

def test_srv_max_hosts_kwarg(self):
client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/")
client._connect()
self.assertGreater(len(client.topology_description.server_descriptions()), 1)
client = self.simple_client("mongodb+srv://test1.test.build.10gen.cc/", srvmaxhosts=1)
client._connect()
self.assertEqual(len(client.topology_description.server_descriptions()), 1)
client = self.simple_client(
"mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1", srvmaxhosts=2
)
client._connect()
self.assertEqual(len(client.topology_description.server_descriptions()), 2)

@unittest.skipIf(
Expand Down
37 changes: 13 additions & 24 deletions test/test_dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,35 +183,24 @@ def create_tests(cls):

class TestParsingErrors(PyMongoTestCase):
def test_invalid_host(self):
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: mongodb is not",
self.simple_client,
"mongodb+srv://mongodb",
)
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: mongodb.com is not",
self.simple_client,
"mongodb+srv://mongodb.com",
)
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: an IP address is not",
self.simple_client,
"mongodb+srv://127.0.0.1",
)
self.assertRaisesRegex(
ConfigurationError,
"Invalid URI host: an IP address is not",
self.simple_client,
"mongodb+srv://[::1]",
)
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb is not"):
client = self.simple_client("mongodb+srv://mongodb")
client._connect()
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: mongodb.com is not"):
client = self.simple_client("mongodb+srv://mongodb.com")
client._connect()
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: an IP address is not"):
client = self.simple_client("mongodb+srv://127.0.0.1")
client._connect()
with self.assertRaisesRegex(ConfigurationError, "Invalid URI host: an IP address is not"):
client = self.simple_client("mongodb+srv://[::1]")
client._connect()


class TestCaseInsensitive(IntegrationTest):
def test_connect_case_insensitive(self):
client = self.simple_client("mongodb+srv://TEST1.TEST.BUILD.10GEN.cc/")
client._connect()
self.assertGreater(len(client.topology_description.server_descriptions()), 1)


Expand Down
Loading