Skip to content

Commit 11c3f3a

Browse files
committed
Fix handling of safe=False PYTHON-358
1 parent daaf8c7 commit 11c3f3a

File tree

4 files changed

+95
-27
lines changed

4 files changed

+95
-27
lines changed

pymongo/collection.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def __set_uuid_subtype(self, subtype):
186186
a UUID used for this collection.""")
187187

188188
def save(self, to_save, manipulate=True,
189-
safe=False, check_keys=True, **kwargs):
189+
safe=None, check_keys=True, **kwargs):
190190
"""Save a document in this collection.
191191
192192
If `to_save` already has an ``"_id"`` then an :meth:`update`
@@ -239,7 +239,7 @@ def save(self, to_save, manipulate=True,
239239
return to_save.get("_id", None)
240240

241241
def insert(self, doc_or_docs, manipulate=True,
242-
safe=False, check_keys=True, continue_on_error=False, **kwargs):
242+
safe=None, check_keys=True, continue_on_error=False, **kwargs):
243243
"""Insert a document(s) into this collection.
244244
245245
If `manipulate` is ``True``, the document(s) are manipulated using
@@ -302,21 +302,17 @@ def insert(self, doc_or_docs, manipulate=True,
302302
if manipulate:
303303
docs = [self.__database._fix_incoming(doc, self) for doc in docs]
304304

305-
if self.safe or kwargs:
306-
safe = True
307-
if not kwargs:
308-
kwargs.update(self.get_lasterror_options())
309-
305+
safe, options = self._get_safe_and_lasterror_options(safe, **kwargs)
310306
self.__database.connection._send_message(
311307
message.insert(self.__full_name, docs,
312-
check_keys, safe, kwargs,
308+
check_keys, safe, options,
313309
continue_on_error, self.__uuid_subtype), safe)
314310

315311
ids = [doc.get("_id", None) for doc in docs]
316312
return return_one and ids[0] or ids
317313

318314
def update(self, spec, document, upsert=False, manipulate=False,
319-
safe=False, multi=False, _check_keys=False, **kwargs):
315+
safe=None, multi=False, _check_keys=False, **kwargs):
320316
"""Update a document(s) in this collection.
321317
322318
Raises :class:`TypeError` if either `spec` or `document` is
@@ -398,17 +394,14 @@ def update(self, spec, document, upsert=False, manipulate=False,
398394
if manipulate:
399395
document = self.__database._fix_incoming(document, self)
400396

401-
if self.safe or kwargs:
402-
safe = True
403-
if not kwargs:
404-
kwargs.update(self.get_lasterror_options())
397+
safe, options = self._get_safe_and_lasterror_options(safe, **kwargs)
405398

406399
# _check_keys is used by save() so we don't upsert pre-existing
407400
# documents after adding an invalid key like 'a.b'. It can't really
408401
# be used for any other update operations.
409402
return self.__database.connection._send_message(
410403
message.update(self.__full_name, upsert, multi,
411-
spec, document, safe, kwargs,
404+
spec, document, safe, options,
412405
_check_keys, self.__uuid_subtype), safe)
413406

414407
def drop(self):
@@ -423,7 +416,7 @@ def drop(self):
423416
"""
424417
self.__database.drop_collection(self.__name)
425418

426-
def remove(self, spec_or_id=None, safe=False, **kwargs):
419+
def remove(self, spec_or_id=None, safe=None, **kwargs):
427420
"""Remove a document(s) from this collection.
428421
429422
.. warning:: Calls to :meth:`remove` should be performed with
@@ -478,14 +471,10 @@ def remove(self, spec_or_id=None, safe=False, **kwargs):
478471
if not isinstance(spec_or_id, dict):
479472
spec_or_id = {"_id": spec_or_id}
480473

481-
if self.safe or kwargs:
482-
safe = True
483-
if not kwargs:
484-
kwargs.update(self.get_lasterror_options())
485-
474+
safe, options = self._get_safe_and_lasterror_options(safe, **kwargs)
486475
return self.__database.connection._send_message(
487-
message.delete(self.__full_name, spec_or_id,
488-
safe, kwargs, self.__uuid_subtype), safe)
476+
message.delete(self.__full_name, spec_or_id, safe,
477+
options, self.__uuid_subtype), safe)
489478

490479
def find_one(self, spec_or_id=None, *args, **kwargs):
491480
"""Get a single document from the database.

pymongo/common.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def validate_positive_float(option, value):
9898

9999
return value
100100

101-
101+
102102
def validate_timeout_or_none(option, value):
103103
"""Validates a timeout specified in milliseconds returning
104104
a value in floating point seconds.
@@ -195,7 +195,7 @@ def __init__(self, **options):
195195
self.__read_pref = ReadPreference.PRIMARY
196196
self.__tag_sets = [{}]
197197
self.__secondary_acceptable_latency_ms = 15
198-
self.__safe = False
198+
self.__safe = None
199199
self.__safe_opts = {}
200200
self.__set_options(options)
201201
if (self.__read_pref == ReadPreference.PRIMARY
@@ -204,6 +204,14 @@ def __init__(self, **options):
204204
raise ConfigurationError(
205205
"ReadPreference PRIMARY cannot be combined with tags")
206206

207+
# If safe hasn't been implicitly set by write concerns then set it.
208+
if self.__safe is None:
209+
self.__safe = validate_boolean('safe', options.get("safe", False))
210+
if self.__safe and not options.get("safe", True):
211+
warnings.warn("Conflicting write concerns. Safe set as False "
212+
"but write concerns have been set making safe True. "
213+
"Please set safe to True.", UserWarning)
214+
207215
def __set_safe_option(self, option, value, check=False):
208216
"""Validates and sets getlasterror options for this
209217
object (Connection, Database, Collection, etc.)
@@ -231,8 +239,6 @@ def __set_options(self, options):
231239
):
232240
self.__secondary_acceptable_latency_ms = \
233241
validate_positive_float(option, value)
234-
elif option == 'safe':
235-
self.__safe = validate_boolean(option, value)
236242
elif option in SAFE_OPTIONS:
237243
if option == 'journal':
238244
self.__set_safe_option('j', value)
@@ -272,7 +278,7 @@ def __set_read_pref(self, value):
272278
self.__read_pref = validate_read_preference('read_preference', value)
273279

274280
read_preference = property(__get_read_pref, __set_read_pref)
275-
281+
276282
def __get_acceptable_latency(self):
277283
"""Any replica-set member whose ping time is within
278284
secondary_acceptable_latency_ms of the nearest member may accept
@@ -366,3 +372,25 @@ def unset_lasterror_options(self, *options):
366372
self.__safe_opts.pop(option, None)
367373
else:
368374
self.__safe_opts = {}
375+
376+
def _get_safe_and_lasterror_options(self, safe=None, **options):
377+
"""Get the current safe mode and any getLastError options.
378+
379+
Determines if the current write is safe or not based on the
380+
passed in or inherited safe value. Passing any write concerns
381+
automatically sets safe to True.
382+
383+
:Parameters:
384+
- `safe`: check that the operation succeeded?
385+
- `**options`: overriding getLastError options
386+
387+
.. versionadded:: 2.2.1+
388+
"""
389+
if safe is None:
390+
safe = self.safe
391+
safe = validate_boolean('safe', safe)
392+
if safe or options:
393+
safe = True
394+
if not options:
395+
options.update(self.get_lasterror_options())
396+
return safe, options

test/test_collection.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ def test_insert_multiple(self):
696696
def test_insert_multiple_with_duplicate(self):
697697
db = self.db
698698
db.drop_collection("test")
699+
db.safe = True
699700
db.test.ensure_index([('i', ASCENDING)], unique=True)
700701

701702
# No error
@@ -706,11 +707,38 @@ def test_insert_multiple_with_duplicate(self):
706707
db.test.insert([{'i': 1}] * 2, safe=False)
707708
self.assertEqual(1, db.test.count())
708709

710+
self.assertRaises(
711+
DuplicateKeyError,
712+
lambda: db.test.insert([{'i': 2}] * 2),
713+
)
714+
715+
# Test based on default setting as False
716+
db.drop_collection("test")
717+
db.safe = False
718+
db.test.ensure_index([('i', ASCENDING)], unique=True)
719+
720+
# No error
721+
db.test.insert([{'i': 1}] * 2)
722+
self.assertEqual(1, db.test.count())
723+
724+
# Implied safe
725+
self.assertRaises(
726+
DuplicateKeyError,
727+
lambda: db.test.insert([{'i': 2}] * 2, w=1),
728+
)
729+
730+
# Explicit safe
709731
self.assertRaises(
710732
DuplicateKeyError,
711733
lambda: db.test.insert([{'i': 2}] * 2, safe=True),
712734
)
713735

736+
# Misconfigured value for safe
737+
self.assertRaises(
738+
TypeError,
739+
lambda: db.test.insert([{'i': 2}] * 2, safe=1),
740+
)
741+
714742
def test_insert_iterables(self):
715743
db = self.db
716744

test/test_common.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,32 @@ class TestCommon(unittest.TestCase):
3232

3333
def test_baseobject(self):
3434

35+
# In Python 2.6+ we could use the catch_warnings context
36+
# manager to test this warning nicely. As we can't do that
37+
# we must test raising errors before the ignore filter is applied.
38+
warnings.simplefilter("error", UserWarning)
39+
self.assertRaises(UserWarning, lambda:
40+
Connection(wtimeout=1000, safe=False))
41+
try:
42+
Connection(wtimeout=1000, safe=True)
43+
except UserWarning:
44+
self.assertFalse(True)
45+
46+
try:
47+
Connection(wtimeout=1000)
48+
except UserWarning:
49+
self.assertFalse(True)
50+
51+
warnings.resetwarnings()
52+
3553
warnings.simplefilter("ignore")
3654

3755
c = Connection(pair)
3856
self.assertFalse(c.slave_okay)
3957
self.assertFalse(c.safe)
4058
self.assertEqual({}, c.get_lasterror_options())
4159
db = c.test
60+
db.drop_collection("test")
4261
self.assertFalse(db.slave_okay)
4362
self.assertFalse(db.safe)
4463
self.assertEqual({}, db.get_lasterror_options())
@@ -51,6 +70,9 @@ def test_baseobject(self):
5170
cursor = coll.find(slave_okay=True)
5271
self.assertTrue(cursor._Cursor__slave_okay)
5372

73+
# Setting any safe operations overrides explicit safe
74+
self.assertTrue(Connection(wtimeout=1000, safe=False).safe)
75+
5476
c = Connection(pair, slaveok=True, w='majority',
5577
wtimeout=300, fsync=True, j=True)
5678
self.assertTrue(c.slave_okay)
@@ -154,6 +176,7 @@ def test_baseobject(self):
154176
# Succeeds since we override the lasterror settings per query.
155177
self.assertTrue(coll.insert({'foo': 'bar'}, fsync=True))
156178
drop_collections(db)
179+
157180
warnings.resetwarnings()
158181

159182

0 commit comments

Comments
 (0)