From 3f8c104157fdff9ddd2bcd85b707d8216046510d Mon Sep 17 00:00:00 2001 From: Prashant Mital <5883388+prashantmital@users.noreply.github.com> Date: Thu, 29 Jul 2021 10:32:51 -0700 Subject: [PATCH] PYTHON-2288 Remove IsMaster (#690) --- doc/changelog.rst | 2 + doc/migrate-to-pymongo4.rst | 6 + pymongo/hello.py | 185 ++++++++++++++++++++++++ pymongo/ismaster.py | 198 -------------------------- pymongo/monitor.py | 4 +- pymongo/pool.py | 5 +- pymongo/server_description.py | 4 +- pymongo/topology.py | 4 +- test/pymongo_mocks.py | 4 +- test/test_discovery_and_monitoring.py | 4 +- test/test_heartbeat_monitoring.py | 6 +- test/test_sdam_monitoring_spec.py | 4 +- test/test_server.py | 4 +- test/test_server_description.py | 4 +- test/test_topology.py | 10 +- test/utils_selection_tests.py | 6 +- 16 files changed, 222 insertions(+), 228 deletions(-) delete mode 100644 pymongo/ismaster.py diff --git a/doc/changelog.rst b/doc/changelog.rst index 5c943407d..f8c610606 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -55,6 +55,8 @@ Breaking Changes in 4.0 - Removed :meth:`pymongo.mongo_client.MongoClient.set_cursor_manager`. - Removed :mod:`pymongo.thread_util`. - Removed :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. +- Removed :class:`~pymongo.ismaster.IsMaster`. + Use :class:`~pymongo.hello.Hello` instead. - Removed :mod:`pymongo.son_manipulator`, :class:`pymongo.son_manipulator.SONManipulator`, :class:`pymongo.son_manipulator.ObjectIdInjector`, diff --git a/doc/migrate-to-pymongo4.rst b/doc/migrate-to-pymongo4.rst index b64ada865..a08eeb5ff 100644 --- a/doc/migrate-to-pymongo4.rst +++ b/doc/migrate-to-pymongo4.rst @@ -378,6 +378,12 @@ custom types to BSON, the :class:`~bson.codec_options.TypeCodec` and For more information, see the :doc:`custom type example `. +IsMaster is removed +------------------- + +Removed :class:`pymongo.ismaster.IsMaster`. +Use :class:`pymongo.hello.Hello` instead. + NotMasterError is removed ------------------------- diff --git a/pymongo/hello.py b/pymongo/hello.py index 34963a40c..290f27dcc 100644 --- a/pymongo/hello.py +++ b/pymongo/hello.py @@ -14,9 +14,194 @@ """Helpers for the 'hello' and legacy hello commands.""" +import itertools + +from pymongo import common +from pymongo.server_type import SERVER_TYPE + class HelloCompat: CMD = 'hello' LEGACY_CMD = 'ismaster' PRIMARY = 'isWritablePrimary' LEGACY_PRIMARY = 'ismaster' + + +def _get_server_type(doc): + """Determine the server type from a hello response.""" + if not doc.get('ok'): + return SERVER_TYPE.Unknown + + if doc.get('serviceId'): + return SERVER_TYPE.LoadBalancer + elif doc.get('isreplicaset'): + return SERVER_TYPE.RSGhost + elif doc.get('setName'): + if doc.get('hidden'): + return SERVER_TYPE.RSOther + elif doc.get(HelloCompat.PRIMARY): + return SERVER_TYPE.RSPrimary + elif doc.get(HelloCompat.LEGACY_PRIMARY): + return SERVER_TYPE.RSPrimary + elif doc.get('secondary'): + return SERVER_TYPE.RSSecondary + elif doc.get('arbiterOnly'): + return SERVER_TYPE.RSArbiter + else: + return SERVER_TYPE.RSOther + elif doc.get('msg') == 'isdbgrid': + return SERVER_TYPE.Mongos + else: + return SERVER_TYPE.Standalone + + +class Hello(object): + """Parse a hello response from the server. + + .. versionadded:: 3.12 + """ + __slots__ = ('_doc', '_server_type', '_is_writable', '_is_readable', + '_awaitable') + + def __init__(self, doc, awaitable=False): + self._server_type = _get_server_type(doc) + self._doc = doc + self._is_writable = self._server_type in ( + SERVER_TYPE.RSPrimary, + SERVER_TYPE.Standalone, + SERVER_TYPE.Mongos, + SERVER_TYPE.LoadBalancer) + + self._is_readable = ( + self.server_type == SERVER_TYPE.RSSecondary + or self._is_writable) + self._awaitable = awaitable + + @property + def document(self): + """The complete hello command response document. + + .. versionadded:: 3.4 + """ + return self._doc.copy() + + @property + def server_type(self): + return self._server_type + + @property + def all_hosts(self): + """List of hosts, passives, and arbiters known to this server.""" + return set(map(common.clean_node, itertools.chain( + self._doc.get('hosts', []), + self._doc.get('passives', []), + self._doc.get('arbiters', [])))) + + @property + def tags(self): + """Replica set member tags or empty dict.""" + return self._doc.get('tags', {}) + + @property + def primary(self): + """This server's opinion about who the primary is, or None.""" + if self._doc.get('primary'): + return common.partition_node(self._doc['primary']) + else: + return None + + @property + def replica_set_name(self): + """Replica set name or None.""" + return self._doc.get('setName') + + @property + def max_bson_size(self): + return self._doc.get('maxBsonObjectSize', common.MAX_BSON_SIZE) + + @property + def max_message_size(self): + return self._doc.get('maxMessageSizeBytes', 2 * self.max_bson_size) + + @property + def max_write_batch_size(self): + return self._doc.get('maxWriteBatchSize', common.MAX_WRITE_BATCH_SIZE) + + @property + def min_wire_version(self): + return self._doc.get('minWireVersion', common.MIN_WIRE_VERSION) + + @property + def max_wire_version(self): + return self._doc.get('maxWireVersion', common.MAX_WIRE_VERSION) + + @property + def set_version(self): + return self._doc.get('setVersion') + + @property + def election_id(self): + return self._doc.get('electionId') + + @property + def cluster_time(self): + return self._doc.get('$clusterTime') + + @property + def logical_session_timeout_minutes(self): + return self._doc.get('logicalSessionTimeoutMinutes') + + @property + def is_writable(self): + return self._is_writable + + @property + def is_readable(self): + return self._is_readable + + @property + def me(self): + me = self._doc.get('me') + if me: + return common.clean_node(me) + + @property + def last_write_date(self): + return self._doc.get('lastWrite', {}).get('lastWriteDate') + + @property + def compressors(self): + return self._doc.get('compression') + + @property + def sasl_supported_mechs(self): + """Supported authentication mechanisms for the current user. + + For example:: + + >>> hello.sasl_supported_mechs + ["SCRAM-SHA-1", "SCRAM-SHA-256"] + + """ + return self._doc.get('saslSupportedMechs', []) + + @property + def speculative_authenticate(self): + """The speculativeAuthenticate field.""" + return self._doc.get('speculativeAuthenticate') + + @property + def topology_version(self): + return self._doc.get('topologyVersion') + + @property + def awaitable(self): + return self._awaitable + + @property + def service_id(self): + return self._doc.get('serviceId') + + @property + def hello_ok(self): + return self._doc.get('helloOk', False) diff --git a/pymongo/ismaster.py b/pymongo/ismaster.py deleted file mode 100644 index e3a8f7f06..000000000 --- a/pymongo/ismaster.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright 2014-present MongoDB, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Parse a response to the 'ismaster' command.""" - -import itertools - -from pymongo import common -from pymongo.hello import HelloCompat -from pymongo.server_type import SERVER_TYPE - - -def _get_server_type(doc): - """Determine the server type from an ismaster response.""" - if not doc.get('ok'): - return SERVER_TYPE.Unknown - - if doc.get('serviceId'): - return SERVER_TYPE.LoadBalancer - elif doc.get('isreplicaset'): - return SERVER_TYPE.RSGhost - elif doc.get('setName'): - if doc.get('hidden'): - return SERVER_TYPE.RSOther - elif doc.get(HelloCompat.PRIMARY): - return SERVER_TYPE.RSPrimary - elif doc.get(HelloCompat.LEGACY_PRIMARY): - return SERVER_TYPE.RSPrimary - elif doc.get('secondary'): - return SERVER_TYPE.RSSecondary - elif doc.get('arbiterOnly'): - return SERVER_TYPE.RSArbiter - else: - return SERVER_TYPE.RSOther - elif doc.get('msg') == 'isdbgrid': - return SERVER_TYPE.Mongos - else: - return SERVER_TYPE.Standalone - - -class IsMaster(object): - __slots__ = ('_doc', '_server_type', '_is_writable', '_is_readable', - '_awaitable') - - def __init__(self, doc, awaitable=False): - """Parse an ismaster response from the server.""" - self._server_type = _get_server_type(doc) - self._doc = doc - self._is_writable = self._server_type in ( - SERVER_TYPE.RSPrimary, - SERVER_TYPE.Standalone, - SERVER_TYPE.Mongos, - SERVER_TYPE.LoadBalancer) - - self._is_readable = ( - self.server_type == SERVER_TYPE.RSSecondary - or self._is_writable) - self._awaitable = awaitable - - @property - def document(self): - """The complete ismaster command response document. - - .. versionadded:: 3.4 - """ - return self._doc.copy() - - @property - def server_type(self): - return self._server_type - - @property - def all_hosts(self): - """List of hosts, passives, and arbiters known to this server.""" - return set(map(common.clean_node, itertools.chain( - self._doc.get('hosts', []), - self._doc.get('passives', []), - self._doc.get('arbiters', [])))) - - @property - def tags(self): - """Replica set member tags or empty dict.""" - return self._doc.get('tags', {}) - - @property - def primary(self): - """This server's opinion about who the primary is, or None.""" - if self._doc.get('primary'): - return common.partition_node(self._doc['primary']) - else: - return None - - @property - def replica_set_name(self): - """Replica set name or None.""" - return self._doc.get('setName') - - @property - def max_bson_size(self): - return self._doc.get('maxBsonObjectSize', common.MAX_BSON_SIZE) - - @property - def max_message_size(self): - return self._doc.get('maxMessageSizeBytes', 2 * self.max_bson_size) - - @property - def max_write_batch_size(self): - return self._doc.get('maxWriteBatchSize', common.MAX_WRITE_BATCH_SIZE) - - @property - def min_wire_version(self): - return self._doc.get('minWireVersion', common.MIN_WIRE_VERSION) - - @property - def max_wire_version(self): - return self._doc.get('maxWireVersion', common.MAX_WIRE_VERSION) - - @property - def set_version(self): - return self._doc.get('setVersion') - - @property - def election_id(self): - return self._doc.get('electionId') - - @property - def cluster_time(self): - return self._doc.get('$clusterTime') - - @property - def logical_session_timeout_minutes(self): - return self._doc.get('logicalSessionTimeoutMinutes') - - @property - def is_writable(self): - return self._is_writable - - @property - def is_readable(self): - return self._is_readable - - @property - def me(self): - me = self._doc.get('me') - if me: - return common.clean_node(me) - - @property - def last_write_date(self): - return self._doc.get('lastWrite', {}).get('lastWriteDate') - - @property - def compressors(self): - return self._doc.get('compression') - - @property - def sasl_supported_mechs(self): - """Supported authentication mechanisms for the current user. - - For example:: - - >>> ismaster.sasl_supported_mechs - ["SCRAM-SHA-1", "SCRAM-SHA-256"] - - """ - return self._doc.get('saslSupportedMechs', []) - - @property - def speculative_authenticate(self): - """The speculativeAuthenticate field.""" - return self._doc.get('speculativeAuthenticate') - - @property - def topology_version(self): - return self._doc.get('topologyVersion') - - @property - def awaitable(self): - return self._awaitable - - @property - def service_id(self): - return self._doc.get('serviceId') - - @property - def hello_ok(self): - return self._doc.get('helloOk', False) diff --git a/pymongo/monitor.py b/pymongo/monitor.py index 8a60abce0..4817ad240 100644 --- a/pymongo/monitor.py +++ b/pymongo/monitor.py @@ -23,7 +23,7 @@ from pymongo import common, periodic_executor from pymongo.errors import (NotPrimaryError, OperationFailure, _OperationCancelled) -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.periodic_executor import _shutdown_executors from pymongo.read_preferences import MovingAverage from pymongo.server_description import ServerDescription @@ -268,7 +268,7 @@ class Monitor(MonitorBase): start = time.monotonic() if conn.more_to_come: # Read the next streaming isMaster (MongoDB 4.4+). - response = IsMaster(conn._next_reply(), awaitable=True) + response = Hello(conn._next_reply(), awaitable=True) elif (conn.performed_handshake and self._server_description.topology_version): # Initiate streaming isMaster (MongoDB 4.4+). diff --git a/pymongo/pool.py b/pymongo/pool.py index 58f1652e0..6dfd32ef0 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -50,8 +50,7 @@ from pymongo.errors import (AutoReconnect, NotPrimaryError, OperationFailure, PyMongoError) -from pymongo.hello import HelloCompat -from pymongo.ismaster import IsMaster +from pymongo.hello import HelloCompat, Hello from pymongo.monitoring import (ConnectionCheckOutFailedReason, ConnectionClosedReason) from pymongo.network import (command, @@ -618,7 +617,7 @@ class SocketInfo(object): doc.setdefault('serviceId', process_id) if not self.opts.load_balanced: doc.pop('serviceId', None) - ismaster = IsMaster(doc, awaitable=awaitable) + ismaster = Hello(doc, awaitable=awaitable) self.is_writable = ismaster.is_writable self.max_wire_version = ismaster.max_wire_version self.max_bson_size = ismaster.max_bson_size diff --git a/pymongo/server_description.py b/pymongo/server_description.py index 19cc349c7..462ad135c 100644 --- a/pymongo/server_description.py +++ b/pymongo/server_description.py @@ -18,7 +18,7 @@ import time from bson import EPOCH_NAIVE from pymongo.server_type import SERVER_TYPE -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello class ServerDescription(object): @@ -48,7 +48,7 @@ class ServerDescription(object): error=None): self._address = address if not ismaster: - ismaster = IsMaster({}) + ismaster = Hello({}) self._server_type = ismaster.server_type self._all_hosts = ismaster.all_hosts diff --git a/pymongo/topology.py b/pymongo/topology.py index dd8315a6d..e08c12ab0 100644 --- a/pymongo/topology.py +++ b/pymongo/topology.py @@ -34,7 +34,7 @@ from pymongo.errors import (ConnectionFailure, PyMongoError, ServerSelectionTimeoutError, WriteError) -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.monitor import SrvMonitor from pymongo.pool import PoolOptions from pymongo.server import Server @@ -567,7 +567,7 @@ class Topology(object): # Emit initial SDAM events for load balancer mode. self._process_change(ServerDescription( self._seed_addresses[0], - IsMaster({'ok': 1, 'serviceId': self._topology_id, + Hello({'ok': 1, 'serviceId': self._topology_id, 'maxWireVersion': 13}))) # Ensure that the monitors are open. diff --git a/test/pymongo_mocks.py b/test/pymongo_mocks.py index af59e0cbe..511560b59 100644 --- a/test/pymongo_mocks.py +++ b/test/pymongo_mocks.py @@ -21,7 +21,7 @@ import weakref from pymongo import common from pymongo import MongoClient from pymongo.errors import AutoReconnect, NetworkTimeout -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.monitor import Monitor from pymongo.pool import Pool from pymongo.server_description import ServerDescription @@ -100,7 +100,7 @@ class MockMonitor(Monitor): client = self.client address = self._server_description.address response, rtt = client.mock_is_master('%s:%d' % address) - return ServerDescription(address, IsMaster(response), rtt) + return ServerDescription(address, Hello(response), rtt) class MockClient(MongoClient): diff --git a/test/test_discovery_and_monitoring.py b/test/test_discovery_and_monitoring.py index 2a440cdf3..0b3ac6da5 100644 --- a/test/test_discovery_and_monitoring.py +++ b/test/test_discovery_and_monitoring.py @@ -31,7 +31,7 @@ from pymongo.errors import (AutoReconnect, OperationFailure) from pymongo.helpers import (_check_command_response, _check_write_command_response) -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.server_description import ServerDescription, SERVER_TYPE from pymongo.settings import TopologySettings from pymongo.topology import Topology, _ErrorContext @@ -83,7 +83,7 @@ def create_mock_topology(uri, monitor_class=DummyMonitor): def got_ismaster(topology, server_address, ismaster_response): server_description = ServerDescription( - server_address, IsMaster(ismaster_response), 0) + server_address, Hello(ismaster_response), 0) topology.on_change(server_description) diff --git a/test/test_heartbeat_monitoring.py b/test/test_heartbeat_monitoring.py index 8c6655702..bd926f772 100644 --- a/test/test_heartbeat_monitoring.py +++ b/test/test_heartbeat_monitoring.py @@ -20,7 +20,7 @@ import threading sys.path[0:0] = [""] from pymongo.errors import ConnectionFailure -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.monitor import Monitor from test import unittest, client_knobs, IntegrationTest from test.utils import (HeartbeatEventListener, MockPool, single_client, @@ -38,7 +38,7 @@ class TestHeartbeatMonitoring(IntegrationTest): def _check_with_socket(self, *args, **kwargs): if isinstance(responses[1], Exception): raise responses[1] - return IsMaster(responses[1]), 99 + return Hello(responses[1]), 99 m = single_client( h=uri, @@ -62,7 +62,7 @@ class TestHeartbeatMonitoring(IntegrationTest): self.assertEqual(actual.connection_id, responses[0]) if expected != 'ServerHeartbeatStartedEvent': - if isinstance(actual.reply, IsMaster): + if isinstance(actual.reply, Hello): self.assertEqual(actual.duration, 99) self.assertEqual(actual.reply._doc, responses[1]) else: diff --git a/test/test_sdam_monitoring_spec.py b/test/test_sdam_monitoring_spec.py index 3afdd2357..cb168bb2b 100644 --- a/test/test_sdam_monitoring_spec.py +++ b/test/test_sdam_monitoring_spec.py @@ -26,7 +26,7 @@ from pymongo import monitoring from pymongo.common import clean_node from pymongo.errors import (ConnectionFailure, NotPrimaryError) -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.monitor import Monitor from pymongo.server_description import ServerDescription from pymongo.topology_description import TOPOLOGY_TYPE @@ -197,7 +197,7 @@ def create_test(scenario_def): source_address = clean_node(source) topology.on_change(ServerDescription( address=source_address, - ismaster=IsMaster(response), + ismaster=Hello(response), round_trip_time=0)) expected_results = phase['outcome']['events'] diff --git a/test/test_server.py b/test/test_server.py index d6b92e2fd..ca3ce76cd 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -18,7 +18,7 @@ import sys sys.path[0:0] = [""] -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.server import Server from pymongo.server_description import ServerDescription from test import unittest @@ -26,7 +26,7 @@ from test import unittest class TestServer(unittest.TestCase): def test_repr(self): - ismaster = IsMaster({'ok': 1}) + ismaster = Hello({'ok': 1}) sd = ServerDescription(('localhost', 27017), ismaster) server = Server(sd, pool=object(), monitor=object()) self.assertTrue('Standalone' in str(server)) diff --git a/test/test_server_description.py b/test/test_server_description.py index 11f134464..27ea5bf37 100644 --- a/test/test_server_description.py +++ b/test/test_server_description.py @@ -21,7 +21,7 @@ sys.path[0:0] = [""] from bson.objectid import ObjectId from bson.int64 import Int64 from pymongo.server_type import SERVER_TYPE -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.server_description import ServerDescription from test import unittest @@ -29,7 +29,7 @@ address = ('localhost', 27017) def parse_ismaster_response(doc): - ismaster_response = IsMaster(doc) + ismaster_response = Hello(doc) return ServerDescription(address, ismaster_response) diff --git a/test/test_topology.py b/test/test_topology.py index 9a4bf512c..4a3018b72 100644 --- a/test/test_topology.py +++ b/test/test_topology.py @@ -29,7 +29,7 @@ from pymongo.topology_description import TOPOLOGY_TYPE from pymongo.errors import (AutoReconnect, ConfigurationError, ConnectionFailure) -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.monitor import Monitor from pymongo.pool import PoolOptions from pymongo.server_description import ServerDescription @@ -67,7 +67,7 @@ def create_mock_topology( def got_ismaster(topology, server_address, ismaster_response): server_description = ServerDescription( - server_address, IsMaster(ismaster_response), 0) + server_address, Hello(ismaster_response), 0) topology.on_change(server_description) @@ -212,7 +212,7 @@ class TestSingleServerTopology(TopologyTest): class TestMonitor(Monitor): def _check_with_socket(self, *args, **kwargs): if available: - return (IsMaster({'ok': 1, 'maxWireVersion': 6}), + return (Hello({'ok': 1, 'maxWireVersion': 6}), round_trip_time) else: raise AutoReconnect('mock monitor error') @@ -673,7 +673,7 @@ class TestTopologyErrors(TopologyTest): def _check_with_socket(self, *args, **kwargs): ismaster_count[0] += 1 if ismaster_count[0] == 1: - return IsMaster({'ok': 1, 'maxWireVersion': 6}), 0 + return Hello({'ok': 1, 'maxWireVersion': 6}), 0 else: raise AutoReconnect('mock monitor error') @@ -695,7 +695,7 @@ class TestTopologyErrors(TopologyTest): def _check_with_socket(self, *args, **kwargs): ismaster_count[0] += 1 if ismaster_count[0] in (1, 3): - return IsMaster({'ok': 1, 'maxWireVersion': 6}), 0 + return Hello({'ok': 1, 'maxWireVersion': 6}), 0 else: raise AutoReconnect( 'mock monitor error #%s' % (ismaster_count[0],)) diff --git a/test/utils_selection_tests.py b/test/utils_selection_tests.py index 57a267303..4037705ae 100644 --- a/test/utils_selection_tests.py +++ b/test/utils_selection_tests.py @@ -23,7 +23,7 @@ sys.path[0:0] = [""] from bson import json_util from pymongo.common import clean_node, HEARTBEAT_FREQUENCY from pymongo.errors import AutoReconnect, ConfigurationError -from pymongo.ismaster import IsMaster +from pymongo.hello import Hello from pymongo.server_description import ServerDescription from pymongo.settings import TopologySettings from pymongo.server_selectors import writable_server_selector @@ -60,7 +60,7 @@ def make_server_description(server, hosts): """Make a ServerDescription from server info in a JSON test.""" server_type = server['type'] if server_type in ("Unknown", "PossiblePrimary"): - return ServerDescription(clean_node(server['address']), IsMaster({})) + return ServerDescription(clean_node(server['address']), Hello({})) ismaster_response = {'ok': True, 'hosts': hosts} if server_type != "Standalone" and server_type != "Mongos": @@ -85,7 +85,7 @@ def make_server_description(server, hosts): # Sets _last_update_time to now. sd = ServerDescription(clean_node(server['address']), - IsMaster(ismaster_response), + Hello(ismaster_response), round_trip_time=server['avg_rtt_ms'] / 1000.0) if 'lastUpdateTime' in server: