Skip to content

Commit c63c068

Browse files
committed
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.
1 parent 981e392 commit c63c068

File tree

9 files changed

+124
-11
lines changed

9 files changed

+124
-11
lines changed

doc/api/pymongo/driver_info.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:mod:`driver_info`
2+
==================
3+
4+
.. automodule:: pymongo.driver_info
5+
6+
.. autoclass:: pymongo.driver_info.DriverInfo(name=None, version=None, platform=None)

doc/api/pymongo/index.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,26 @@ Sub-modules:
3131
.. toctree::
3232
:maxdepth: 2
3333

34-
database
34+
bulk
3535
change_stream
3636
client_session
3737
collation
3838
collection
3939
command_cursor
4040
cursor
41-
bulk
41+
cursor_manager
42+
database
43+
driver_info
4244
errors
4345
message
44-
monitoring
4546
mongo_client
4647
mongo_replica_set_client
48+
monitoring
4749
operations
4850
pool
4951
read_concern
5052
read_preferences
5153
results
5254
son_manipulator
53-
cursor_manager
5455
uri_parser
5556
write_concern

pymongo/client_options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def _parse_pool_options(options):
121121
wait_queue_multiple = options.get('waitqueuemultiple')
122122
event_listeners = options.get('event_listeners')
123123
appname = options.get('appname')
124+
driver = options.get('driver')
124125
compression_settings = CompressionSettings(
125126
options.get('compressors', []),
126127
options.get('zlibcompressionlevel', -1))
@@ -133,6 +134,7 @@ def _parse_pool_options(options):
133134
ssl_context, ssl_match_hostname, socket_keepalive,
134135
_EventListeners(event_listeners),
135136
appname,
137+
driver,
136138
compression_settings)
137139

138140

pymongo/common.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
"""Functions and classes common to multiple pymongo modules."""
1717

18-
import collections
1918
import datetime
2019
import warnings
2120

@@ -28,6 +27,7 @@
2827
from pymongo.auth import MECHANISMS
2928
from pymongo.compression_support import (validate_compressors,
3029
validate_zlib_compression_level)
30+
from pymongo.driver_info import DriverInfo
3131
from pymongo.errors import ConfigurationError
3232
from pymongo.monitoring import _validate_event_listeners
3333
from pymongo.read_concern import ReadConcern
@@ -464,6 +464,15 @@ def validate_appname_or_none(option, value):
464464
return value
465465

466466

467+
def validate_driver_or_none(option, value):
468+
"""Validate the driver keyword arg."""
469+
if value is None:
470+
return value
471+
if not isinstance(value, DriverInfo):
472+
raise TypeError("%s must be an instance of DriverInfo" % (option,))
473+
return value
474+
475+
467476
def validate_ok_for_replace(replacement):
468477
"""Validate a replacement document."""
469478
validate_is_mapping("replacement", replacement)
@@ -539,6 +548,7 @@ def validate_tzinfo(dummy, value):
539548
'connect': validate_boolean_or_string,
540549
'minpoolsize': validate_non_negative_integer,
541550
'appname': validate_appname_or_none,
551+
'driver': validate_driver_or_none,
542552
'unicode_decode_error_handler': validate_unicode_decode_error_handler,
543553
'retrywrites': validate_boolean_or_string,
544554
'compressors': validate_compressors,

pymongo/driver_info.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2018-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you
4+
# may not use this file except in compliance with the License. You
5+
# may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
# implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
15+
"""Advanced options for MongoDB drivers implemented on top of PyMongo."""
16+
17+
from collections import namedtuple
18+
19+
from bson.py3compat import string_type
20+
21+
22+
class DriverInfo(namedtuple('DriverInfo', ['name', 'version', 'platform'])):
23+
"""Info about a driver wrapping PyMongo.
24+
25+
The MongoDB server logs PyMongo's name, version, and platform whenever
26+
PyMongo establishes a connection. A driver implemented on top of PyMongo
27+
can add its own info to this log message. Initialize with three strings
28+
like 'MyDriver', '1.2.3', 'some platform info'. Any of these strings may be
29+
None to accept PyMongo's default.
30+
"""
31+
def __new__(cls, name=None, version=None, platform=None):
32+
self = super(DriverInfo, cls).__new__(cls, name, version, platform)
33+
for name, value in self._asdict().items():
34+
if value is not None and not isinstance(value, string_type):
35+
raise TypeError("Wrong type for DriverInfo %s option, value "
36+
"must be an instance of %s" % (
37+
name, string_type.__name__))
38+
39+
return self

pymongo/mongo_client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ def __init__(
224224
print this value in the server log upon establishing each
225225
connection. It is also recorded in the slow query log and
226226
profile collections.
227+
- `driver`: (pair or None) A driver implemented on top of PyMongo can
228+
pass a :class:`~pymongo.driver_info.DriverInfo` to add its name,
229+
version, and platform to the message printed in the server log when
230+
establishing a connection.
227231
- `event_listeners`: a list or tuple of event listeners. See
228232
:mod:`~pymongo.monitoring` for details.
229233
- `retryWrites`: (boolean) Whether supported write operations
@@ -400,6 +404,9 @@ def __init__(
400404
Added support for mongodb+srv:// URIs.
401405
Added the ``retryWrites`` keyword argument and URI option.
402406
407+
.. versionchanged:: 3.7
408+
Added the ``driver`` keyword argument.
409+
403410
.. versionchanged:: 3.5
404411
Add ``username`` and ``password`` options. Document the
405412
``authSource``, ``authMechanism``, and ``authMechanismProperties ``

pymongo/pool.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# permissions and limitations under the License.
1414

1515
import contextlib
16+
import copy
1617
import os
1718
import platform
1819
import socket
@@ -280,15 +281,15 @@ class PoolOptions(object):
280281
'__connect_timeout', '__socket_timeout',
281282
'__wait_queue_timeout', '__wait_queue_multiple',
282283
'__ssl_context', '__ssl_match_hostname', '__socket_keepalive',
283-
'__event_listeners', '__appname', '__metadata',
284+
'__event_listeners', '__appname', '__driver', '__metadata',
284285
'__compression_settings')
285286

286287
def __init__(self, max_pool_size=100, min_pool_size=0,
287288
max_idle_time_seconds=None, connect_timeout=None,
288289
socket_timeout=None, wait_queue_timeout=None,
289290
wait_queue_multiple=None, ssl_context=None,
290291
ssl_match_hostname=True, socket_keepalive=True,
291-
event_listeners=None, appname=None,
292+
event_listeners=None, appname=None, driver=None,
292293
compression_settings=None):
293294

294295
self.__max_pool_size = max_pool_size
@@ -303,11 +304,31 @@ def __init__(self, max_pool_size=100, min_pool_size=0,
303304
self.__socket_keepalive = socket_keepalive
304305
self.__event_listeners = event_listeners
305306
self.__appname = appname
307+
self.__driver = driver
306308
self.__compression_settings = compression_settings
307-
self.__metadata = _METADATA.copy()
309+
self.__metadata = copy.deepcopy(_METADATA)
308310
if appname:
309311
self.__metadata['application'] = {'name': appname}
310312

313+
# Combine the "driver" MongoClient option with PyMongo's info, like:
314+
# {
315+
# 'driver': {
316+
# 'name': 'PyMongo|MyDriver',
317+
# 'version': '3.7.0|1.2.3',
318+
# },
319+
# 'platform': 'CPython 3.6.0|MyPlatform'
320+
# }
321+
if driver:
322+
if driver.name:
323+
self.__metadata['driver']['name'] = "%s|%s" % (
324+
_METADATA['driver']['name'], driver.name)
325+
if driver.version:
326+
self.__metadata['driver']['version'] = "%s|%s" % (
327+
_METADATA['driver']['version'], driver.version)
328+
if driver.platform:
329+
self.__metadata['platform'] = "%s|%s" % (
330+
_METADATA['platform'], driver.platform)
331+
311332
@property
312333
def max_pool_size(self):
313334
"""The maximum allowable number of concurrent connections to each
@@ -395,6 +416,12 @@ def appname(self):
395416
"""
396417
return self.__appname
397418

419+
@property
420+
def driver(self):
421+
"""Driver name and version, for sending with ismaster in handshake.
422+
"""
423+
return self.__driver
424+
398425
@property
399426
def compression_settings(self):
400427
return self.__compression_settings

pymongo/topology.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,8 @@ def _create_pool_for_monitor(self, address):
551551
ssl_context=options.ssl_context,
552552
ssl_match_hostname=options.ssl_match_hostname,
553553
event_listeners=options.event_listeners,
554-
appname=options.appname)
554+
appname=options.appname,
555+
driver=options.driver)
555556

556557
return self._settings.pool_class(address, monitor_pool_options,
557558
handshake=False)

test/test_client.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
"""Test the mongo_client module."""
1616

1717
import contextlib
18+
import copy
1819
import datetime
1920
import gc
2021
import os
21-
import platform
2222
import signal
2323
import socket
2424
import struct
@@ -50,6 +50,7 @@
5050
from pymongo.monitoring import (ServerHeartbeatListener,
5151
ServerHeartbeatStartedEvent)
5252
from pymongo.mongo_client import MongoClient
53+
from pymongo.driver_info import DriverInfo
5354
from pymongo.pool import SocketInfo, _METADATA
5455
from pymongo.read_preferences import ReadPreference
5556
from pymongo.server_selectors import (any_server_selector,
@@ -214,7 +215,7 @@ def test_read_preference(self):
214215
self.assertEqual(c.read_preference, ReadPreference.NEAREST)
215216

216217
def test_metadata(self):
217-
metadata = _METADATA.copy()
218+
metadata = copy.deepcopy(_METADATA)
218219
metadata['application'] = {'name': 'foobar'}
219220
client = MongoClient(
220221
"mongodb://foo:27017/?appname=foobar&connect=false")
@@ -226,6 +227,25 @@ def test_metadata(self):
226227
# No error
227228
MongoClient(appname='x' * 128)
228229
self.assertRaises(ValueError, MongoClient, appname='x' * 129)
230+
# Bad "driver" options.
231+
self.assertRaises(TypeError, DriverInfo, 'Foo', 1, 'a')
232+
self.assertRaises(TypeError, MongoClient, driver=1)
233+
self.assertRaises(TypeError, MongoClient, driver='abc')
234+
self.assertRaises(TypeError, MongoClient, driver=('Foo', '1', 'a'))
235+
# Test appending to driver info.
236+
metadata['driver']['name'] = 'PyMongo|FooDriver'
237+
metadata['driver']['version'] = '%s|1.2.3' % (
238+
_METADATA['driver']['version'],)
239+
client = MongoClient('foo', 27017, appname='foobar',
240+
driver=DriverInfo('FooDriver', '1.2.3', None), connect=False)
241+
options = client._MongoClient__options
242+
self.assertEqual(options.pool_options.metadata, metadata)
243+
metadata['platform'] = '%s|FooPlatform' % (
244+
_METADATA['platform'],)
245+
client = MongoClient('foo', 27017, appname='foobar',
246+
driver=DriverInfo('FooDriver', '1.2.3', 'FooPlatform'), connect=False)
247+
options = client._MongoClient__options
248+
self.assertEqual(options.pool_options.metadata, metadata)
229249

230250
def test_kwargs_codec_options(self):
231251
# Ensure codec options are passed in correctly

0 commit comments

Comments
 (0)