Skip to content

Commit 193915e

Browse files
committed
PYTHON-959 - Connection string spec compliance.
1 parent b9baa8a commit 193915e

14 files changed

+1663
-79
lines changed

pymongo/client_options.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ class ClientOptions(object):
107107
def __init__(self, username, password, database, options):
108108
self.__options = options
109109

110-
options = dict([validate(opt, val) for opt, val in iteritems(options)])
111-
112110
self.__codec_options = _parse_codec_options(options)
113111
self.__credentials = _parse_credentials(
114112
username, password, database, options)

pymongo/common.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
"""Functions and classes common to multiple pymongo modules."""
1717

1818
import collections
19+
import warnings
1920

2021
from bson.binary import (STANDARD, PYTHON_LEGACY,
2122
JAVA_LEGACY, CSHARP_LEGACY)
2223
from bson.codec_options import CodecOptions
23-
from bson.py3compat import string_type, integer_types
24+
from bson.py3compat import string_type, integer_types, iteritems
2425
from pymongo.auth import MECHANISMS
2526
from pymongo.errors import ConfigurationError
2627
from pymongo.read_preferences import (read_pref_mode_from_name,
@@ -74,6 +75,7 @@
7475
# Error codes to ignore if GridFS calls createIndex on a secondary
7576
UNAUTHORIZED_CODES = (13, 16547, 16548)
7677

78+
7779
def partition_node(node):
7880
"""Split a host:port string into (host, int(port)) pair."""
7981
host = node
@@ -340,15 +342,16 @@ def validate_auth_mechanism_properties(option, value):
340342
for opt in value.split(','):
341343
try:
342344
key, val = opt.split(':')
343-
if key not in _MECHANISM_PROPS:
344-
raise ValueError("%s is not a supported auth "
345-
"mechanism property. Must be one of "
346-
"%s." % (key, tuple(_MECHANISM_PROPS)))
347-
props[key] = val
348345
except ValueError:
349346
raise ValueError("auth mechanism properties must be "
350347
"key:value pairs like SERVICE_NAME:"
351348
"mongodb, not %s." % (opt,))
349+
if key not in _MECHANISM_PROPS:
350+
raise ValueError("%s is not a supported auth "
351+
"mechanism property. Must be one of "
352+
"%s." % (key, tuple(_MECHANISM_PROPS)))
353+
props[key] = val
354+
352355
return props
353356

354357

@@ -456,6 +459,23 @@ def validate(option, value):
456459
return lower, value
457460

458461

462+
def get_validated_options(options):
463+
"""Validate each entry in options and raise a warning if it is not valid.
464+
Returns a copy of options with invalid entries removed
465+
"""
466+
validated_options = {}
467+
for opt, value in iteritems(options):
468+
lower = opt.lower()
469+
try:
470+
validator = VALIDATORS.get(lower, raise_config_error)
471+
value = validator(opt, value)
472+
except (ValueError, ConfigurationError) as exc:
473+
warnings.warn(str(exc))
474+
else:
475+
validated_options[lower] = value
476+
return validated_options
477+
478+
459479
WRITE_CONCERN_OPTIONS = frozenset([
460480
'w',
461481
'wtimeout',
@@ -516,4 +536,3 @@ def read_preference(self):
516536
The :attr:`read_preference` attribute is now read only.
517537
"""
518538
return self.__read_preference
519-

pymongo/mongo_client.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def __init__(
300300
for entity in host:
301301
if "://" in entity:
302302
if entity.startswith("mongodb://"):
303-
res = uri_parser.parse_uri(entity, port, False)
303+
res = uri_parser.parse_uri(entity, port, warn=True)
304304
seeds.update(res["nodelist"])
305305
username = res["username"] or username
306306
password = res["password"] or password
@@ -321,10 +321,14 @@ def __init__(
321321
monitor_class = kwargs.pop('_monitor_class', None)
322322
condition_class = kwargs.pop('_condition_class', None)
323323

324-
opts['document_class'] = document_class
325-
opts['tz_aware'] = tz_aware
326-
opts['connect'] = connect
327-
opts.update(kwargs)
324+
keyword_opts = kwargs
325+
keyword_opts['document_class'] = document_class
326+
keyword_opts['tz_aware'] = tz_aware
327+
keyword_opts['connect'] = connect
328+
# Validate all keyword options.
329+
keyword_opts = dict(common.validate(k, v)
330+
for k, v in keyword_opts.items())
331+
opts.update(keyword_opts)
328332
self.__options = options = ClientOptions(
329333
username, password, dbase, opts)
330334

@@ -792,6 +796,9 @@ def option_repr(option, value):
792796
else:
793797
return 'document_class=%s.%s' % (value.__module__,
794798
value.__name__)
799+
if "ms" in option:
800+
return "%s='%s'" % (option, int(value * 1000))
801+
795802
return '%s=%r' % (option, value)
796803

797804
# Host first...

pymongo/uri_parser.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515

1616
"""Tools to parse and validate a MongoDB URI."""
17+
import warnings
1718

1819
from bson.py3compat import PY3, iteritems, string_type
1920

@@ -22,7 +23,7 @@
2223
else:
2324
from urllib import unquote_plus
2425

25-
from pymongo.common import validate as _validate
26+
from pymongo.common import (validate as _validate, get_validated_options)
2627
from pymongo.errors import ConfigurationError, InvalidURI
2728

2829

@@ -129,6 +130,8 @@ def parse_host(entity, default_port=DEFAULT_PORT):
129130
port = default_port
130131
if entity[0] == '[':
131132
host, port = parse_ipv6_literal_host(entity, default_port)
133+
elif entity.endswith(".sock"):
134+
return entity, default_port
132135
elif entity.find(':') != -1:
133136
if entity.count(':') > 1:
134137
raise ValueError("Reserved characters such as ':' must be "
@@ -137,8 +140,9 @@ def parse_host(entity, default_port=DEFAULT_PORT):
137140
"and ']' according to RFC 2732.")
138141
host, port = host.split(':', 1)
139142
if isinstance(port, string_type):
140-
if not port.isdigit():
141-
raise ValueError("Port number must be an integer.")
143+
if not port.isdigit() or int(port) > 65535 or int(port) <= 0:
144+
raise ValueError("Port must be an integer between 0 and 65535: %s"
145+
% (port,))
142146
port = int(port)
143147

144148
# Normalize hostname to lowercase, since DNS is case-insensitive:
@@ -148,15 +152,23 @@ def parse_host(entity, default_port=DEFAULT_PORT):
148152
return host.lower(), port
149153

150154

151-
def validate_options(opts):
155+
def validate_options(opts, warn=False):
152156
"""Validates and normalizes options passed in a MongoDB URI.
153157
154-
Returns a new dictionary of validated and normalized options.
158+
Returns a new dictionary of validated and normalized options. If warn is
159+
False then errors will be thrown for invalid options, otherwise they will
160+
be ignored and a warning will be issued.
155161
156162
:Parameters:
157163
- `opts`: A dict of MongoDB URI options.
164+
- `warn` (optional): If ``True`` then warnigns will be logged and
165+
invalid options will be ignored. Otherwise invalid options will
166+
cause errors.
158167
"""
159-
return dict([_validate(opt, val) for opt, val in iteritems(opts)])
168+
if warn:
169+
return get_validated_options(opts)
170+
else:
171+
return dict([_validate(opt, val) for opt, val in iteritems(opts)])
160172

161173

162174
def _parse_options(opts, delim):
@@ -172,11 +184,21 @@ def _parse_options(opts, delim):
172184
# str(option) to ensure that a unicode URI results in plain 'str'
173185
# option names. 'normalized' is then suitable to be passed as
174186
# kwargs in all Python versions.
175-
options[str(key)] = val
187+
if str(key) in options:
188+
warnings.warn("Duplicate URI option %s" % (str(key),))
189+
options[str(key)] = unquote_plus(val)
190+
191+
# Special case for deprecated options
192+
if "wtimeout" in options:
193+
if "wtimeoutMS" in options:
194+
options.pop("wtimeout")
195+
warnings.warn("Option wtimeout is deprecated, use 'wtimeoutMS'"
196+
" instead")
197+
176198
return options
177199

178200

179-
def split_options(opts, validate=True):
201+
def split_options(opts, validate=True, warn=False):
180202
"""Takes the options portion of a MongoDB URI, validates each option
181203
and returns the options in a dictionary.
182204
@@ -202,7 +224,7 @@ def split_options(opts, validate=True):
202224
raise InvalidURI("MongoDB URI options are key=value pairs.")
203225

204226
if validate:
205-
return validate_options(options)
227+
return validate_options(options, warn)
206228
return options
207229

208230

@@ -232,7 +254,7 @@ def split_hosts(hosts, default_port=DEFAULT_PORT):
232254
return nodes
233255

234256

235-
def parse_uri(uri, default_port=DEFAULT_PORT, validate=True):
257+
def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False):
236258
"""Parse and validate a MongoDB URI.
237259
238260
Returns a dict of the form::
@@ -252,6 +274,13 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True):
252274
for a host in the URI.
253275
- `validate`: If ``True`` (the default), validate and normalize all
254276
options.
277+
- `warn` (optional): When validating, if ``True`` then will warn
278+
the user then ignore any invalid options or values. If ``False``,
279+
validation will error when options are unsupported or values are
280+
invalid.
281+
282+
.. versionchanged:: 3.1
283+
``warn`` added so invalid options can be ignored.
255284
"""
256285
if not uri.startswith(SCHEME):
257286
raise InvalidURI("Invalid URI scheme: URI "
@@ -262,7 +291,6 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True):
262291
if not scheme_free:
263292
raise InvalidURI("Must provide at least one hostname or IP.")
264293

265-
nodes = None
266294
user = None
267295
passwd = None
268296
dbase = None
@@ -272,11 +300,14 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True):
272300
# Check for unix domain sockets in the uri
273301
if '.sock' in scheme_free:
274302
host_part, _, path_part = _rpartition(scheme_free, '/')
275-
try:
276-
parse_uri('%s%s' % (SCHEME, host_part))
277-
except (ConfigurationError, InvalidURI):
278-
host_part = scheme_free
303+
if not host_part:
304+
host_part = path_part
279305
path_part = ""
306+
if '/' in host_part:
307+
raise InvalidURI("Any '/' in a unix domain socket must be"
308+
" URL encoded: %s" % host_part)
309+
host_part = unquote_plus(host_part)
310+
path_part = unquote_plus(path_part)
280311
else:
281312
host_part, _, path_part = _partition(scheme_free, '/')
282313

@@ -302,7 +333,12 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True):
302333
dbase, collection = dbase.split('.', 1)
303334

304335
if opts:
305-
options = split_options(opts, validate)
336+
options = split_options(opts, validate, warn)
337+
338+
if dbase is not None:
339+
dbase = unquote_plus(dbase)
340+
if collection is not None:
341+
collection = unquote_plus(collection)
306342

307343
return {
308344
'nodelist': nodes,

0 commit comments

Comments
 (0)