From c63c068611bd4ea3387fc1a08082a96869ac82a1 Mon Sep 17 00:00:00 2001 From: "A. Jesse Jiryu Davis" Date: Tue, 29 May 2018 09:02:46 -0400 Subject: [PATCH] PYTHON-1564 Add DriverInfo to handshake metadata Allow drivers that wrap PyMongo to add their info to the handshake metadata, using a "driver" option like: MongoClient(driver=DriverInfo("MyDriver", "1.2.3")) The DriverInfo is appended to PyMongo's own metadata. --- doc/api/pymongo/driver_info.rst | 6 +++++ doc/api/pymongo/index.rst | 9 ++++---- pymongo/client_options.py | 2 ++ pymongo/common.py | 12 +++++++++- pymongo/driver_info.py | 39 +++++++++++++++++++++++++++++++++ pymongo/mongo_client.py | 7 ++++++ pymongo/pool.py | 33 +++++++++++++++++++++++++--- pymongo/topology.py | 3 ++- test/test_client.py | 24 ++++++++++++++++++-- 9 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 doc/api/pymongo/driver_info.rst create mode 100644 pymongo/driver_info.py diff --git a/doc/api/pymongo/driver_info.rst b/doc/api/pymongo/driver_info.rst new file mode 100644 index 000000000..9e6f73557 --- /dev/null +++ b/doc/api/pymongo/driver_info.rst @@ -0,0 +1,6 @@ +:mod:`driver_info` +================== + +.. automodule:: pymongo.driver_info + + .. autoclass:: pymongo.driver_info.DriverInfo(name=None, version=None, platform=None) diff --git a/doc/api/pymongo/index.rst b/doc/api/pymongo/index.rst index e33d7a267..cc47fad78 100644 --- a/doc/api/pymongo/index.rst +++ b/doc/api/pymongo/index.rst @@ -31,25 +31,26 @@ Sub-modules: .. toctree:: :maxdepth: 2 - database + bulk change_stream client_session collation collection command_cursor cursor - bulk + cursor_manager + database + driver_info errors message - monitoring mongo_client mongo_replica_set_client + monitoring operations pool read_concern read_preferences results son_manipulator - cursor_manager uri_parser write_concern diff --git a/pymongo/client_options.py b/pymongo/client_options.py index b4232b8c1..b91e1d88c 100644 --- a/pymongo/client_options.py +++ b/pymongo/client_options.py @@ -121,6 +121,7 @@ def _parse_pool_options(options): wait_queue_multiple = options.get('waitqueuemultiple') event_listeners = options.get('event_listeners') appname = options.get('appname') + driver = options.get('driver') compression_settings = CompressionSettings( options.get('compressors', []), options.get('zlibcompressionlevel', -1)) @@ -133,6 +134,7 @@ def _parse_pool_options(options): ssl_context, ssl_match_hostname, socket_keepalive, _EventListeners(event_listeners), appname, + driver, compression_settings) diff --git a/pymongo/common.py b/pymongo/common.py index 9e05f02ef..8f11ec0df 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -15,7 +15,6 @@ """Functions and classes common to multiple pymongo modules.""" -import collections import datetime import warnings @@ -28,6 +27,7 @@ from bson.raw_bson import RawBSONDocument from pymongo.auth import MECHANISMS from pymongo.compression_support import (validate_compressors, validate_zlib_compression_level) +from pymongo.driver_info import DriverInfo from pymongo.errors import ConfigurationError from pymongo.monitoring import _validate_event_listeners from pymongo.read_concern import ReadConcern @@ -464,6 +464,15 @@ def validate_appname_or_none(option, value): return value +def validate_driver_or_none(option, value): + """Validate the driver keyword arg.""" + if value is None: + return value + if not isinstance(value, DriverInfo): + raise TypeError("%s must be an instance of DriverInfo" % (option,)) + return value + + def validate_ok_for_replace(replacement): """Validate a replacement document.""" validate_is_mapping("replacement", replacement) @@ -539,6 +548,7 @@ URI_VALIDATORS = { 'connect': validate_boolean_or_string, 'minpoolsize': validate_non_negative_integer, 'appname': validate_appname_or_none, + 'driver': validate_driver_or_none, 'unicode_decode_error_handler': validate_unicode_decode_error_handler, 'retrywrites': validate_boolean_or_string, 'compressors': validate_compressors, diff --git a/pymongo/driver_info.py b/pymongo/driver_info.py new file mode 100644 index 000000000..1f5235aca --- /dev/null +++ b/pymongo/driver_info.py @@ -0,0 +1,39 @@ +# Copyright 2018-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. + +"""Advanced options for MongoDB drivers implemented on top of PyMongo.""" + +from collections import namedtuple + +from bson.py3compat import string_type + + +class DriverInfo(namedtuple('DriverInfo', ['name', 'version', 'platform'])): + """Info about a driver wrapping PyMongo. + + The MongoDB server logs PyMongo's name, version, and platform whenever + PyMongo establishes a connection. A driver implemented on top of PyMongo + can add its own info to this log message. Initialize with three strings + like 'MyDriver', '1.2.3', 'some platform info'. Any of these strings may be + None to accept PyMongo's default. + """ + def __new__(cls, name=None, version=None, platform=None): + self = super(DriverInfo, cls).__new__(cls, name, version, platform) + for name, value in self._asdict().items(): + if value is not None and not isinstance(value, string_type): + raise TypeError("Wrong type for DriverInfo %s option, value " + "must be an instance of %s" % ( + name, string_type.__name__)) + + return self diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index cd910598a..fbf3bbe6b 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -224,6 +224,10 @@ class MongoClient(common.BaseObject): print this value in the server log upon establishing each connection. It is also recorded in the slow query log and profile collections. + - `driver`: (pair or None) A driver implemented on top of PyMongo can + pass a :class:`~pymongo.driver_info.DriverInfo` to add its name, + version, and platform to the message printed in the server log when + establishing a connection. - `event_listeners`: a list or tuple of event listeners. See :mod:`~pymongo.monitoring` for details. - `retryWrites`: (boolean) Whether supported write operations @@ -400,6 +404,9 @@ class MongoClient(common.BaseObject): Added support for mongodb+srv:// URIs. Added the ``retryWrites`` keyword argument and URI option. + .. versionchanged:: 3.7 + Added the ``driver`` keyword argument. + .. versionchanged:: 3.5 Add ``username`` and ``password`` options. Document the ``authSource``, ``authMechanism``, and ``authMechanismProperties `` diff --git a/pymongo/pool.py b/pymongo/pool.py index c6a111248..9ea5c1c5f 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. import contextlib +import copy import os import platform import socket @@ -280,7 +281,7 @@ class PoolOptions(object): '__connect_timeout', '__socket_timeout', '__wait_queue_timeout', '__wait_queue_multiple', '__ssl_context', '__ssl_match_hostname', '__socket_keepalive', - '__event_listeners', '__appname', '__metadata', + '__event_listeners', '__appname', '__driver', '__metadata', '__compression_settings') def __init__(self, max_pool_size=100, min_pool_size=0, @@ -288,7 +289,7 @@ class PoolOptions(object): socket_timeout=None, wait_queue_timeout=None, wait_queue_multiple=None, ssl_context=None, ssl_match_hostname=True, socket_keepalive=True, - event_listeners=None, appname=None, + event_listeners=None, appname=None, driver=None, compression_settings=None): self.__max_pool_size = max_pool_size @@ -303,11 +304,31 @@ class PoolOptions(object): self.__socket_keepalive = socket_keepalive self.__event_listeners = event_listeners self.__appname = appname + self.__driver = driver self.__compression_settings = compression_settings - self.__metadata = _METADATA.copy() + self.__metadata = copy.deepcopy(_METADATA) if appname: self.__metadata['application'] = {'name': appname} + # Combine the "driver" MongoClient option with PyMongo's info, like: + # { + # 'driver': { + # 'name': 'PyMongo|MyDriver', + # 'version': '3.7.0|1.2.3', + # }, + # 'platform': 'CPython 3.6.0|MyPlatform' + # } + if driver: + if driver.name: + self.__metadata['driver']['name'] = "%s|%s" % ( + _METADATA['driver']['name'], driver.name) + if driver.version: + self.__metadata['driver']['version'] = "%s|%s" % ( + _METADATA['driver']['version'], driver.version) + if driver.platform: + self.__metadata['platform'] = "%s|%s" % ( + _METADATA['platform'], driver.platform) + @property def max_pool_size(self): """The maximum allowable number of concurrent connections to each @@ -395,6 +416,12 @@ class PoolOptions(object): """ return self.__appname + @property + def driver(self): + """Driver name and version, for sending with ismaster in handshake. + """ + return self.__driver + @property def compression_settings(self): return self.__compression_settings diff --git a/pymongo/topology.py b/pymongo/topology.py index 3891777d5..aca1d91f1 100644 --- a/pymongo/topology.py +++ b/pymongo/topology.py @@ -551,7 +551,8 @@ class Topology(object): ssl_context=options.ssl_context, ssl_match_hostname=options.ssl_match_hostname, event_listeners=options.event_listeners, - appname=options.appname) + appname=options.appname, + driver=options.driver) return self._settings.pool_class(address, monitor_pool_options, handshake=False) diff --git a/test/test_client.py b/test/test_client.py index df24737ac..7e5567e64 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -15,10 +15,10 @@ """Test the mongo_client module.""" import contextlib +import copy import datetime import gc import os -import platform import signal import socket import struct @@ -50,6 +50,7 @@ from pymongo.errors import (AutoReconnect, from pymongo.monitoring import (ServerHeartbeatListener, ServerHeartbeatStartedEvent) from pymongo.mongo_client import MongoClient +from pymongo.driver_info import DriverInfo from pymongo.pool import SocketInfo, _METADATA from pymongo.read_preferences import ReadPreference from pymongo.server_selectors import (any_server_selector, @@ -214,7 +215,7 @@ class ClientUnitTest(unittest.TestCase): self.assertEqual(c.read_preference, ReadPreference.NEAREST) def test_metadata(self): - metadata = _METADATA.copy() + metadata = copy.deepcopy(_METADATA) metadata['application'] = {'name': 'foobar'} client = MongoClient( "mongodb://foo:27017/?appname=foobar&connect=false") @@ -226,6 +227,25 @@ class ClientUnitTest(unittest.TestCase): # No error MongoClient(appname='x' * 128) self.assertRaises(ValueError, MongoClient, appname='x' * 129) + # Bad "driver" options. + self.assertRaises(TypeError, DriverInfo, 'Foo', 1, 'a') + self.assertRaises(TypeError, MongoClient, driver=1) + self.assertRaises(TypeError, MongoClient, driver='abc') + self.assertRaises(TypeError, MongoClient, driver=('Foo', '1', 'a')) + # Test appending to driver info. + metadata['driver']['name'] = 'PyMongo|FooDriver' + metadata['driver']['version'] = '%s|1.2.3' % ( + _METADATA['driver']['version'],) + client = MongoClient('foo', 27017, appname='foobar', + driver=DriverInfo('FooDriver', '1.2.3', None), connect=False) + options = client._MongoClient__options + self.assertEqual(options.pool_options.metadata, metadata) + metadata['platform'] = '%s|FooPlatform' % ( + _METADATA['platform'],) + client = MongoClient('foo', 27017, appname='foobar', + driver=DriverInfo('FooDriver', '1.2.3', 'FooPlatform'), connect=False) + options = client._MongoClient__options + self.assertEqual(options.pool_options.metadata, metadata) def test_kwargs_codec_options(self): # Ensure codec options are passed in correctly