Skip to content

Commit 4178fcc

Browse files
authored
Merge branch 'master' into PYTHON-5309
2 parents 24354b4 + 42cb70e commit 4178fcc

20 files changed

+426
-53
lines changed

doc/changelog.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ Version 4.12.1 is a bug fix release.
1010
- Fixed a bug that could raise ``UnboundLocalError`` when creating asynchronous connections over SSL.
1111
- Fixed a bug causing SRV hostname validation to fail when resolver and resolved hostnames are identical with three domain levels.
1212
- Fixed a bug that caused direct use of ``pymongo.uri_parser`` to raise an ``AttributeError``.
13+
- Fixed a bug where clients created with connect=False and a "mongodb+srv://" connection string
14+
could cause public ``pymongo.MongoClient`` and ``pymongo.AsyncMongoClient`` attributes (topology_description,
15+
nodes, address, primary, secondaries, arbiters) to incorrectly return a Database, leading to type
16+
errors such as: "NotImplementedError: Database objects do not implement truth value testing or bool()".
1317
- Removed Eventlet testing against Python versions newer than 3.9 since
1418
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
1519
- Fixed a bug that would cause AsyncMongoClient to attempt to use PyOpenSSL when available, resulting in errors such as
1620
"pymongo.errors.ServerSelectionTimeoutError: 'SSLContext' object has no attribute 'wrap_bio'"
1721

18-
1922
Issues Resolved
2023
...............
2124

pymongo/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@
106106
from pymongo.write_concern import WriteConcern
107107

108108
# Public module compatibility imports
109-
import pymongo.uri_parser # noqa: F401 # isort: skip
109+
# isort: off
110+
from pymongo import uri_parser # noqa: F401
111+
from pymongo import change_stream # noqa: F401
112+
from pymongo import client_session # noqa: F401
113+
from pymongo import collection # noqa: F401
114+
from pymongo import command_cursor # noqa: F401
115+
from pymongo import database # noqa: F401
116+
# isort: on
110117

111118
version = __version__
112119
"""Current version of PyMongo."""

pymongo/asynchronous/mongo_client.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
)
110110
from pymongo.read_preferences import ReadPreference, _ServerMode
111111
from pymongo.results import ClientBulkWriteResult
112+
from pymongo.server_description import ServerDescription
112113
from pymongo.server_selectors import writable_server_selector
113114
from pymongo.server_type import SERVER_TYPE
114115
from pymongo.topology_description import TOPOLOGY_TYPE, TopologyDescription
@@ -779,7 +780,7 @@ def __init__(
779780
keyword_opts["document_class"] = doc_class
780781
self._resolve_srv_info: dict[str, Any] = {"keyword_opts": keyword_opts}
781782

782-
seeds = set()
783+
self._seeds = set()
783784
is_srv = False
784785
username = None
785786
password = None
@@ -804,18 +805,18 @@ def __init__(
804805
srv_max_hosts=srv_max_hosts,
805806
)
806807
is_srv = entity.startswith(SRV_SCHEME)
807-
seeds.update(res["nodelist"])
808+
self._seeds.update(res["nodelist"])
808809
username = res["username"] or username
809810
password = res["password"] or password
810811
dbase = res["database"] or dbase
811812
opts = res["options"]
812813
fqdn = res["fqdn"]
813814
else:
814-
seeds.update(split_hosts(entity, self._port))
815-
if not seeds:
815+
self._seeds.update(split_hosts(entity, self._port))
816+
if not self._seeds:
816817
raise ConfigurationError("need to specify at least one host")
817818

818-
for hostname in [node[0] for node in seeds]:
819+
for hostname in [node[0] for node in self._seeds]:
819820
if _detect_external_db(hostname):
820821
break
821822

@@ -838,7 +839,7 @@ def __init__(
838839
srv_service_name = opts.get("srvServiceName", common.SRV_SERVICE_NAME)
839840

840841
srv_max_hosts = srv_max_hosts or opts.get("srvmaxhosts")
841-
opts = self._normalize_and_validate_options(opts, seeds)
842+
opts = self._normalize_and_validate_options(opts, self._seeds)
842843

843844
# Username and password passed as kwargs override user info in URI.
844845
username = opts.get("username", username)
@@ -857,7 +858,7 @@ def __init__(
857858
"username": username,
858859
"password": password,
859860
"dbase": dbase,
860-
"seeds": seeds,
861+
"seeds": self._seeds,
861862
"fqdn": fqdn,
862863
"srv_service_name": srv_service_name,
863864
"pool_class": pool_class,
@@ -873,8 +874,7 @@ def __init__(
873874
self._options.read_concern,
874875
)
875876

876-
if not is_srv:
877-
self._init_based_on_options(seeds, srv_max_hosts, srv_service_name)
877+
self._init_based_on_options(self._seeds, srv_max_hosts, srv_service_name)
878878

879879
self._opened = False
880880
self._closed = False
@@ -975,6 +975,7 @@ def _init_based_on_options(
975975
srv_service_name=srv_service_name,
976976
srv_max_hosts=srv_max_hosts,
977977
server_monitoring_mode=self._options.server_monitoring_mode,
978+
topology_id=self._topology_settings._topology_id if self._topology_settings else None,
978979
)
979980
if self._options.auto_encryption_opts:
980981
from pymongo.asynchronous.encryption import _Encrypter
@@ -1205,6 +1206,16 @@ def topology_description(self) -> TopologyDescription:
12051206
12061207
.. versionadded:: 4.0
12071208
"""
1209+
if self._topology is None:
1210+
servers = {(host, port): ServerDescription((host, port)) for host, port in self._seeds}
1211+
return TopologyDescription(
1212+
TOPOLOGY_TYPE.Unknown,
1213+
servers,
1214+
None,
1215+
None,
1216+
None,
1217+
self._topology_settings,
1218+
)
12081219
return self._topology.description
12091220

12101221
@property
@@ -1218,6 +1229,8 @@ def nodes(self) -> FrozenSet[_Address]:
12181229
to any servers, or a network partition causes it to lose connection
12191230
to all servers.
12201231
"""
1232+
if self._topology is None:
1233+
return frozenset()
12211234
description = self._topology.description
12221235
return frozenset(s.address for s in description.known_servers)
12231236

@@ -1576,6 +1589,8 @@ async def address(self) -> Optional[tuple[str, int]]:
15761589
15771590
.. versionadded:: 3.0
15781591
"""
1592+
if self._topology is None:
1593+
await self._get_topology()
15791594
topology_type = self._topology._description.topology_type
15801595
if (
15811596
topology_type == TOPOLOGY_TYPE.Sharded
@@ -1598,6 +1613,8 @@ async def primary(self) -> Optional[tuple[str, int]]:
15981613
.. versionadded:: 3.0
15991614
AsyncMongoClient gained this property in version 3.0.
16001615
"""
1616+
if self._topology is None:
1617+
await self._get_topology()
16011618
return await self._topology.get_primary() # type: ignore[return-value]
16021619

16031620
@property
@@ -1611,6 +1628,8 @@ async def secondaries(self) -> set[_Address]:
16111628
.. versionadded:: 3.0
16121629
AsyncMongoClient gained this property in version 3.0.
16131630
"""
1631+
if self._topology is None:
1632+
await self._get_topology()
16141633
return await self._topology.get_secondaries()
16151634

16161635
@property
@@ -1621,6 +1640,8 @@ async def arbiters(self) -> set[_Address]:
16211640
connected to a replica set, there are no arbiters, or this client was
16221641
created without the `replicaSet` option.
16231642
"""
1643+
if self._topology is None:
1644+
await self._get_topology()
16241645
return await self._topology.get_arbiters()
16251646

16261647
@property

pymongo/asynchronous/pool.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import asyncio
1718
import collections
1819
import contextlib
1920
import logging
@@ -860,8 +861,14 @@ async def _reset(
860861
# PoolClosedEvent but that reset() SHOULD close sockets *after*
861862
# publishing the PoolClearedEvent.
862863
if close:
863-
for conn in sockets:
864-
await conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
864+
if not _IS_SYNC:
865+
await asyncio.gather(
866+
*[conn.close_conn(ConnectionClosedReason.POOL_CLOSED) for conn in sockets],
867+
return_exceptions=True,
868+
)
869+
else:
870+
for conn in sockets:
871+
await conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
865872
if self.enabled_for_cmap:
866873
assert listeners is not None
867874
listeners.publish_pool_closed(self.address)
@@ -891,8 +898,14 @@ async def _reset(
891898
serverPort=self.address[1],
892899
serviceId=service_id,
893900
)
894-
for conn in sockets:
895-
await conn.close_conn(ConnectionClosedReason.STALE)
901+
if not _IS_SYNC:
902+
await asyncio.gather(
903+
*[conn.close_conn(ConnectionClosedReason.STALE) for conn in sockets],
904+
return_exceptions=True,
905+
)
906+
else:
907+
for conn in sockets:
908+
await conn.close_conn(ConnectionClosedReason.STALE)
896909

897910
async def update_is_writable(self, is_writable: Optional[bool]) -> None:
898911
"""Updates the is_writable attribute on all sockets currently in the
@@ -938,8 +951,14 @@ async def remove_stale_sockets(self, reference_generation: int) -> None:
938951
and self.conns[-1].idle_time_seconds() > self.opts.max_idle_time_seconds
939952
):
940953
close_conns.append(self.conns.pop())
941-
for conn in close_conns:
942-
await conn.close_conn(ConnectionClosedReason.IDLE)
954+
if not _IS_SYNC:
955+
await asyncio.gather(
956+
*[conn.close_conn(ConnectionClosedReason.IDLE) for conn in close_conns],
957+
return_exceptions=True,
958+
)
959+
else:
960+
for conn in close_conns:
961+
await conn.close_conn(ConnectionClosedReason.IDLE)
943962

944963
while True:
945964
async with self.size_cond:

pymongo/asynchronous/settings.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(
5151
srv_service_name: str = common.SRV_SERVICE_NAME,
5252
srv_max_hosts: int = 0,
5353
server_monitoring_mode: str = common.SERVER_MONITORING_MODE,
54+
topology_id: Optional[ObjectId] = None,
5455
):
5556
"""Represent MongoClient's configuration.
5657
@@ -78,8 +79,10 @@ def __init__(
7879
self._srv_service_name = srv_service_name
7980
self._srv_max_hosts = srv_max_hosts or 0
8081
self._server_monitoring_mode = server_monitoring_mode
81-
82-
self._topology_id = ObjectId()
82+
if topology_id is not None:
83+
self._topology_id = topology_id
84+
else:
85+
self._topology_id = ObjectId()
8386
# Store the allocation traceback to catch unclosed clients in the
8487
# test suite.
8588
self._stack = "".join(traceback.format_stack()[:-2])

pymongo/asynchronous/topology.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -529,12 +529,6 @@ async def _process_change(
529529
if not _IS_SYNC:
530530
self._monitor_tasks.append(self._srv_monitor)
531531

532-
# Clear the pool from a failed heartbeat.
533-
if reset_pool:
534-
server = self._servers.get(server_description.address)
535-
if server:
536-
await server.pool.reset(interrupt_connections=interrupt_connections)
537-
538532
# Wake anything waiting in select_servers().
539533
self._condition.notify_all()
540534

@@ -557,6 +551,11 @@ async def on_change(
557551
# that didn't include this server.
558552
if self._opened and self._description.has_server(server_description.address):
559553
await self._process_change(server_description, reset_pool, interrupt_connections)
554+
# Clear the pool from a failed heartbeat, done outside the lock to avoid blocking on connection close.
555+
if reset_pool:
556+
server = self._servers.get(server_description.address)
557+
if server:
558+
await server.pool.reset(interrupt_connections=interrupt_connections)
560559

561560
async def _process_srv_update(self, seedlist: list[tuple[str, Any]]) -> None:
562561
"""Process a new seedlist on an opened topology.

0 commit comments

Comments
 (0)