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.
This commit is contained in:
A. Jesse Jiryu Davis 2018-05-29 09:02:46 -04:00
parent 981e39281f
commit c63c068611
9 changed files with 124 additions and 11 deletions

View File

@ -0,0 +1,6 @@
:mod:`driver_info`
==================
.. automodule:: pymongo.driver_info
.. autoclass:: pymongo.driver_info.DriverInfo(name=None, version=None, platform=None)

View File

@ -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

View File

@ -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)

View File

@ -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,

39
pymongo/driver_info.py Normal file
View File

@ -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

View File

@ -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 ``

View File

@ -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

View File

@ -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)

View File

@ -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